segment-animals 1.0.0__py3-none-any.whl
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.
- segment_animals/__init__.py +53 -0
- segment_animals/detect.py +57 -0
- segment_animals/model_cache.py +262 -0
- segment_animals/models.py +25 -0
- segment_animals/segment.py +71 -0
- segment_animals/util.py +33 -0
- segment_animals/viz.py +100 -0
- segment_animals-1.0.0.dist-info/METADATA +75 -0
- segment_animals-1.0.0.dist-info/RECORD +11 -0
- segment_animals-1.0.0.dist-info/WHEEL +4 -0
- segment_animals-1.0.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from typing import Literal
|
|
2
|
+
from PIL import Image
|
|
3
|
+
import torch
|
|
4
|
+
from .detect import DetectionModel, DetectionModelNames
|
|
5
|
+
from .segment import SegmentationModel, SegmentationModelNames
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_default_device() -> Literal["cpu", "cuda", "mps"]:
|
|
9
|
+
"""
|
|
10
|
+
Get the default device for model inference.
|
|
11
|
+
This function checks for CUDA availability first, then MPS (for Apple Silicon),
|
|
12
|
+
and defaults to CPU if neither is available.
|
|
13
|
+
"""
|
|
14
|
+
if torch.cuda.is_available():
|
|
15
|
+
return "cuda"
|
|
16
|
+
elif "mps" in dir(torch.backends) and torch.backends.mps.is_available():
|
|
17
|
+
return "mps"
|
|
18
|
+
else:
|
|
19
|
+
return "cpu"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AutoAnimalSegmenter:
|
|
24
|
+
"""
|
|
25
|
+
AutoAnimalSegmenter is a class that combines detection and segmentation models
|
|
26
|
+
to automatically detect and segment animals in images.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
detection_model_name: DetectionModelNames = "MDV5A",
|
|
32
|
+
detection_threshold: float = 0.15,
|
|
33
|
+
segmentation_model_name: SegmentationModelNames = "vit_h",
|
|
34
|
+
segmentation_device: Literal["cpu", "cuda", "mps"] = get_default_device(),
|
|
35
|
+
):
|
|
36
|
+
self.detector = DetectionModel(
|
|
37
|
+
model_name=detection_model_name, threshold=detection_threshold
|
|
38
|
+
)
|
|
39
|
+
self.segmentor = SegmentationModel(
|
|
40
|
+
model_name=segmentation_model_name, device=segmentation_device
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def process_image(self, image: Image.Image) -> tuple:
|
|
44
|
+
|
|
45
|
+
detections = self.detector.detect(image)
|
|
46
|
+
masks = self.segmentor.segment(image, detections)
|
|
47
|
+
return detections, masks
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def main() -> None:
|
|
51
|
+
print(
|
|
52
|
+
"No main function implemented yet. Use AutoAnimalSegmenter to process images."
|
|
53
|
+
)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
from megadetector.detection import run_detector
|
|
3
|
+
from .models import AnimalDetection
|
|
4
|
+
from typing import List, Literal
|
|
5
|
+
from logging import getLogger
|
|
6
|
+
|
|
7
|
+
logger = getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
DetectionModelNames = Literal["MDV5A", "MDV5B"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DetectionModel:
|
|
13
|
+
"""
|
|
14
|
+
Model for detecting animals in images.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
model_name: str = "MDV5A",
|
|
20
|
+
device: Literal["cpu", "cuda", "mps"] = "cpu",
|
|
21
|
+
threshold: float = 0.15,
|
|
22
|
+
):
|
|
23
|
+
"""
|
|
24
|
+
Initialize the detection model with a specified model name.
|
|
25
|
+
"""
|
|
26
|
+
self.device = torch.device(device)
|
|
27
|
+
logger.info(f"Loading detection model '{model_name}' on device: {self.device}")
|
|
28
|
+
|
|
29
|
+
self.model = run_detector.load_detector(model_name)
|
|
30
|
+
logger.info(f"Model loaded: {self.model}")
|
|
31
|
+
|
|
32
|
+
self.threshold = threshold
|
|
33
|
+
|
|
34
|
+
def detect(self, image) -> List[AnimalDetection]:
|
|
35
|
+
"""
|
|
36
|
+
Detect animals in the provided image.
|
|
37
|
+
|
|
38
|
+
:param image: The input image to process.
|
|
39
|
+
:return: A list of detections above a confidence threshold.
|
|
40
|
+
"""
|
|
41
|
+
result = self.model.generate_detections_one_image(
|
|
42
|
+
image, image_id="", detection_threshold=self.threshold
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
return [
|
|
46
|
+
AnimalDetection(
|
|
47
|
+
bbox=(
|
|
48
|
+
d["bbox"][0] * image.width,
|
|
49
|
+
d["bbox"][1] * image.height,
|
|
50
|
+
d["bbox"][2] * image.width,
|
|
51
|
+
d["bbox"][3] * image.height,
|
|
52
|
+
),
|
|
53
|
+
confidence=d["conf"],
|
|
54
|
+
)
|
|
55
|
+
for d in result["detections"]
|
|
56
|
+
if d["conf"] >= self.threshold and d["category"] == "1"
|
|
57
|
+
]
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Model caching and download utilities for segment_animals.
|
|
3
|
+
|
|
4
|
+
Handles downloading and caching of model weights with hash verification.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import hashlib
|
|
9
|
+
import requests
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Optional, Dict, List
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Model configurations with their download URLs, expected SHA256 hashes, and namespaces
|
|
15
|
+
MODEL_CONFIGS = {
|
|
16
|
+
"vit_h": {
|
|
17
|
+
"url": "https://dl.fbaipublicfiles.com/segment_anything/sam_vit_h_4b8939.pth",
|
|
18
|
+
"filename": "sam_vit_h_4b8939.pth",
|
|
19
|
+
"sha256": "a7bf3b02f3ebf1267aba913ff637d9a2d5c33d3173bb679e46d9f338c26f262e",
|
|
20
|
+
"namespace": "sam",
|
|
21
|
+
},
|
|
22
|
+
"vit_l": {
|
|
23
|
+
"url": "https://dl.fbaipublicfiles.com/segment_anything/sam_vit_l_0b3195.pth",
|
|
24
|
+
"filename": "sam_vit_l_0b3195.pth",
|
|
25
|
+
"sha256": "3adcc4315b642a4d2101128f611684e8734c41232a17c648ed1693702a49a622",
|
|
26
|
+
"namespace": "sam",
|
|
27
|
+
},
|
|
28
|
+
"vit_b": {
|
|
29
|
+
"url": "https://dl.fbaipublicfiles.com/segment_anything/sam_vit_b_01ec64.pth",
|
|
30
|
+
"filename": "sam_vit_b_01ec64.pth",
|
|
31
|
+
"sha256": "ec2df62732614e57411cdcf32a23ffdf28910380d03139ee0f4fcbe91eb8c912",
|
|
32
|
+
"namespace": "sam",
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_cache_dir(model_namespace: str = "sam") -> Path:
|
|
38
|
+
"""
|
|
39
|
+
Get the cache directory for a specific model namespace within segment_animals.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
model_namespace: The namespace for the models (e.g., 'sam', 'yolo', etc.)
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Path to the cache directory for the specified namespace
|
|
46
|
+
"""
|
|
47
|
+
if os.name == "nt": # Windows
|
|
48
|
+
cache_base = os.environ.get(
|
|
49
|
+
"LOCALAPPDATA", os.path.expanduser("~\\AppData\\Local")
|
|
50
|
+
)
|
|
51
|
+
else: # macOS and Linux
|
|
52
|
+
cache_base = os.environ.get("XDG_CACHE_HOME", os.path.expanduser("~/.cache"))
|
|
53
|
+
|
|
54
|
+
cache_dir = Path(cache_base) / "segment_animals" / "models" / model_namespace
|
|
55
|
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
56
|
+
return cache_dir
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def calculate_file_hash(file_path: Path) -> str:
|
|
60
|
+
"""
|
|
61
|
+
Calculate SHA256 hash of a file.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
file_path: Path to the file
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Hexadecimal SHA256 hash string
|
|
68
|
+
"""
|
|
69
|
+
sha256_hash = hashlib.sha256()
|
|
70
|
+
with open(file_path, "rb") as f:
|
|
71
|
+
# Read file in chunks to handle large files efficiently
|
|
72
|
+
for chunk in iter(lambda: f.read(4096), b""):
|
|
73
|
+
sha256_hash.update(chunk)
|
|
74
|
+
return sha256_hash.hexdigest()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def verify_file_hash(file_path: Path, expected_hash: str) -> bool:
|
|
78
|
+
"""
|
|
79
|
+
Verify that a file matches the expected SHA256 hash.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
file_path: Path to the file to verify
|
|
83
|
+
expected_hash: Expected SHA256 hash
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
True if hash matches, False otherwise
|
|
87
|
+
"""
|
|
88
|
+
if not file_path.exists():
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
actual_hash = calculate_file_hash(file_path)
|
|
92
|
+
return actual_hash.lower() == expected_hash.lower()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def download_file_with_progress(url: str, destination: Path) -> None:
|
|
96
|
+
"""
|
|
97
|
+
Download a file with progress indication.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
url: URL to download from
|
|
101
|
+
destination: Path where to save the file
|
|
102
|
+
"""
|
|
103
|
+
print(f"Downloading from {url}...")
|
|
104
|
+
|
|
105
|
+
response = requests.get(url, stream=True)
|
|
106
|
+
response.raise_for_status()
|
|
107
|
+
|
|
108
|
+
total_size = int(response.headers.get("content-length", 0))
|
|
109
|
+
downloaded_size = 0
|
|
110
|
+
|
|
111
|
+
with open(destination, "wb") as f:
|
|
112
|
+
for chunk in response.iter_content(chunk_size=8192):
|
|
113
|
+
if chunk:
|
|
114
|
+
f.write(chunk)
|
|
115
|
+
downloaded_size += len(chunk)
|
|
116
|
+
|
|
117
|
+
if total_size > 0:
|
|
118
|
+
progress = (downloaded_size / total_size) * 100
|
|
119
|
+
print(f"\rProgress: {progress:.1f}%", end="", flush=True)
|
|
120
|
+
|
|
121
|
+
if total_size > 0:
|
|
122
|
+
print() # New line after progress
|
|
123
|
+
print(f"Download completed: {destination}")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def get_model_path(model_name: str, auto_download: bool = True) -> Path:
|
|
127
|
+
"""
|
|
128
|
+
Get the path to a cached model, downloading it if necessary.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
model_name: Name of the model (e.g., 'sam_vit_h_4b8939')
|
|
132
|
+
auto_download: Whether to automatically download the model if not found
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Path to the model file
|
|
136
|
+
|
|
137
|
+
Raises:
|
|
138
|
+
ValueError: If model_name is not recognized
|
|
139
|
+
FileNotFoundError: If model is not cached and auto_download is False
|
|
140
|
+
RuntimeError: If download fails or hash verification fails
|
|
141
|
+
"""
|
|
142
|
+
if model_name not in MODEL_CONFIGS:
|
|
143
|
+
raise ValueError(
|
|
144
|
+
f"Unknown model: {model_name}. Available models: {list(MODEL_CONFIGS.keys())}"
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
config = MODEL_CONFIGS[model_name]
|
|
148
|
+
model_namespace = config.get(
|
|
149
|
+
"namespace", "sam"
|
|
150
|
+
) # Default to 'sam' for backward compatibility
|
|
151
|
+
cache_dir = get_cache_dir(model_namespace)
|
|
152
|
+
model_path = cache_dir / config["filename"]
|
|
153
|
+
|
|
154
|
+
# Check if model exists and has correct hash
|
|
155
|
+
if model_path.exists():
|
|
156
|
+
print(f"Found cached model: {model_path}")
|
|
157
|
+
if verify_file_hash(model_path, config["sha256"]):
|
|
158
|
+
print("Hash verification passed")
|
|
159
|
+
return model_path
|
|
160
|
+
else:
|
|
161
|
+
print("Hash verification failed, re-downloading...")
|
|
162
|
+
model_path.unlink() # Remove corrupted file
|
|
163
|
+
|
|
164
|
+
if not auto_download:
|
|
165
|
+
raise FileNotFoundError(
|
|
166
|
+
f"Model {model_name} not found in cache and auto_download is disabled"
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# Download the model
|
|
170
|
+
try:
|
|
171
|
+
print(f"Downloading {model_name} model...")
|
|
172
|
+
download_file_with_progress(config["url"], model_path)
|
|
173
|
+
|
|
174
|
+
# Verify hash after download
|
|
175
|
+
if not verify_file_hash(model_path, config["sha256"]):
|
|
176
|
+
model_path.unlink() # Remove corrupted download
|
|
177
|
+
raise RuntimeError(
|
|
178
|
+
f"Downloaded file hash verification failed for {model_name}"
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
print("Hash verification passed")
|
|
182
|
+
return model_path
|
|
183
|
+
|
|
184
|
+
except Exception as e:
|
|
185
|
+
if model_path.exists():
|
|
186
|
+
model_path.unlink() # Clean up partial download
|
|
187
|
+
raise RuntimeError(f"Failed to download {model_name}: {e}")
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def register_model(
|
|
191
|
+
model_name: str, url: str, filename: str, sha256: str, namespace: str = "sam"
|
|
192
|
+
) -> None:
|
|
193
|
+
"""
|
|
194
|
+
Register a new model in the model registry.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
model_name: Unique identifier for the model
|
|
198
|
+
url: Download URL for the model
|
|
199
|
+
filename: Local filename to save the model as
|
|
200
|
+
sha256: Expected SHA256 hash of the model file
|
|
201
|
+
namespace: Model namespace (default: 'sam')
|
|
202
|
+
"""
|
|
203
|
+
MODEL_CONFIGS[model_name] = {
|
|
204
|
+
"url": url,
|
|
205
|
+
"filename": filename,
|
|
206
|
+
"sha256": sha256,
|
|
207
|
+
"namespace": namespace,
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def list_models(namespace: Optional[str] = None) -> List[str]:
|
|
212
|
+
"""
|
|
213
|
+
List available models, optionally filtered by namespace.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
namespace: Optional namespace to filter by
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
List of model names
|
|
220
|
+
"""
|
|
221
|
+
if namespace is None:
|
|
222
|
+
return list(MODEL_CONFIGS.keys())
|
|
223
|
+
|
|
224
|
+
return [
|
|
225
|
+
model_name
|
|
226
|
+
for model_name, config in MODEL_CONFIGS.items()
|
|
227
|
+
if config.get("namespace", "sam") == namespace
|
|
228
|
+
]
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def list_namespaces() -> List[str]:
|
|
232
|
+
"""
|
|
233
|
+
List all available model namespaces.
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
List of unique namespaces
|
|
237
|
+
"""
|
|
238
|
+
namespaces = set()
|
|
239
|
+
for config in MODEL_CONFIGS.values():
|
|
240
|
+
namespaces.add(config.get("namespace", "sam"))
|
|
241
|
+
return sorted(list(namespaces))
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def get_model_info(model_name: str) -> Dict[str, str]:
|
|
245
|
+
"""
|
|
246
|
+
Get information about a specific model.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
model_name: Name of the model
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
Dictionary with model information
|
|
253
|
+
|
|
254
|
+
Raises:
|
|
255
|
+
ValueError: If model_name is not recognized
|
|
256
|
+
"""
|
|
257
|
+
if model_name not in MODEL_CONFIGS:
|
|
258
|
+
raise ValueError(
|
|
259
|
+
f"Unknown model: {model_name}. Available models: {list(MODEL_CONFIGS.keys())}"
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
return MODEL_CONFIGS[model_name].copy()
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from typing import Annotated, Tuple
|
|
2
|
+
from pydantic import BaseModel
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class AnimalDetection(BaseModel):
|
|
6
|
+
"""
|
|
7
|
+
Model representing an animal detection.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
bbox: Annotated[
|
|
11
|
+
Tuple[float, float, float, float],
|
|
12
|
+
"Bounding box coordinates (x_min, y_min, x_max, y_max)",
|
|
13
|
+
]
|
|
14
|
+
confidence: Annotated[float, "Confidence score of the detection (0.0 to 1.0)"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AnimalSegment(BaseModel):
|
|
18
|
+
"""
|
|
19
|
+
Model representing an animal segmentation.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
mask: Annotated[
|
|
23
|
+
list[list[float]],
|
|
24
|
+
"Segmentation mask as a binary array",
|
|
25
|
+
]
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from typing import List, Literal
|
|
2
|
+
from segment_anything import SamPredictor, sam_model_registry
|
|
3
|
+
from PIL import Image
|
|
4
|
+
import numpy as np
|
|
5
|
+
import torch
|
|
6
|
+
from segment_animals.models import AnimalDetection
|
|
7
|
+
from segment_animals.model_cache import get_model_path
|
|
8
|
+
from segment_anything.utils.transforms import ResizeLongestSide
|
|
9
|
+
|
|
10
|
+
SegmentationModelNames = Literal["vit_h", "vit_l", "vit_b"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SegmentationModel:
|
|
14
|
+
"""
|
|
15
|
+
Model for segmenting animals in images using SAM (Segment Anything Model).
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
model_name: SegmentationModelNames = "vit_h",
|
|
21
|
+
device: Literal["cpu", "cuda", "mps"] = "cpu",
|
|
22
|
+
):
|
|
23
|
+
# Get the model path, downloading it if necessary
|
|
24
|
+
model_path = get_model_path(model_name, auto_download=True)
|
|
25
|
+
|
|
26
|
+
self.sam = sam_model_registry[model_name](checkpoint=str(model_path))
|
|
27
|
+
self.sam.to(device)
|
|
28
|
+
self.predictor = SamPredictor(self.sam)
|
|
29
|
+
self.resize_transform = ResizeLongestSide(self.sam.image_encoder.img_size)
|
|
30
|
+
|
|
31
|
+
def segment(self, image: Image.Image, detections: List[AnimalDetection]):
|
|
32
|
+
"""
|
|
33
|
+
Segment animals in the provided image.
|
|
34
|
+
|
|
35
|
+
:param image: The input image to process.
|
|
36
|
+
:param detections: The detections to guide the segmentation.
|
|
37
|
+
:return: A list of segmentation masks.
|
|
38
|
+
"""
|
|
39
|
+
np_image = np.array(image.convert("RGB"))
|
|
40
|
+
self.predictor.set_image(np_image)
|
|
41
|
+
|
|
42
|
+
if not detections:
|
|
43
|
+
return torch.empty((0, *np_image.shape[:2]), dtype=torch.bool)
|
|
44
|
+
|
|
45
|
+
transformed_boxes = self.resize_transform.apply_boxes_torch(
|
|
46
|
+
torch.tensor(
|
|
47
|
+
np.atleast_2d(
|
|
48
|
+
np.array(
|
|
49
|
+
[
|
|
50
|
+
[
|
|
51
|
+
d.bbox[0],
|
|
52
|
+
d.bbox[1],
|
|
53
|
+
d.bbox[0] + d.bbox[2],
|
|
54
|
+
d.bbox[1] + d.bbox[3],
|
|
55
|
+
]
|
|
56
|
+
for d in detections
|
|
57
|
+
]
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
),
|
|
61
|
+
np_image.shape[:2],
|
|
62
|
+
).to(self.sam.device)
|
|
63
|
+
|
|
64
|
+
masks, _, _ = self.predictor.predict_torch(
|
|
65
|
+
point_coords=None,
|
|
66
|
+
point_labels=None,
|
|
67
|
+
boxes=transformed_boxes,
|
|
68
|
+
multimask_output=False,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
return masks.cpu()
|
segment_animals/util.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from PIL import Image
|
|
2
|
+
from requests import get
|
|
3
|
+
from io import BytesIO
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
ImageSource = str | Path | Image.Image
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def load_image(source: ImageSource) -> Image.Image:
|
|
10
|
+
"""
|
|
11
|
+
Load an image from a file path or URL.
|
|
12
|
+
|
|
13
|
+
:param source: The file path or URL of the image.
|
|
14
|
+
:return: The loaded image as a PIL Image object.
|
|
15
|
+
"""
|
|
16
|
+
if isinstance(source, Image.Image):
|
|
17
|
+
# If the source is already a PIL Image, return a copy of it
|
|
18
|
+
return source.copy()
|
|
19
|
+
|
|
20
|
+
elif isinstance(source, (Path, str)) and Path(source).is_file():
|
|
21
|
+
# If the source is a file path, open the image from the file
|
|
22
|
+
return Image.open(source)
|
|
23
|
+
|
|
24
|
+
elif isinstance(source, str):
|
|
25
|
+
# If the source is a URL, fetch the image from the URL
|
|
26
|
+
response = get(source)
|
|
27
|
+
response.raise_for_status()
|
|
28
|
+
return Image.open(BytesIO(response.content))
|
|
29
|
+
|
|
30
|
+
else:
|
|
31
|
+
raise ValueError(
|
|
32
|
+
"Invalid image source. Provide a file path, URL, or PIL Image object."
|
|
33
|
+
)
|
segment_animals/viz.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from matplotlib import pyplot as plt
|
|
2
|
+
from matplotlib.patches import Rectangle
|
|
3
|
+
import numpy as np
|
|
4
|
+
from PIL import Image
|
|
5
|
+
import random
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def thumbnail(image, size=512):
|
|
9
|
+
"""
|
|
10
|
+
Create a thumbnail of the image with the specified size.
|
|
11
|
+
The thumbnail will maintain the aspect ratio of the original image.
|
|
12
|
+
"""
|
|
13
|
+
image = image.copy()
|
|
14
|
+
image.thumbnail((size, size))
|
|
15
|
+
return image
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def random_bright_rgb_0_1():
|
|
19
|
+
"""
|
|
20
|
+
Generate a random bright RGB color with values between 0 and 1.
|
|
21
|
+
The color will be bright enough to be visible against most backgrounds.
|
|
22
|
+
"""
|
|
23
|
+
return tuple(random.randint(128, 255) / 255 for _ in range(3))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def plot_detections_and_masks(image, detections, masks):
|
|
27
|
+
"""
|
|
28
|
+
Plot the detections and masks on the image.
|
|
29
|
+
"""
|
|
30
|
+
plt.figure(figsize=(10, 10))
|
|
31
|
+
plt.imshow(image)
|
|
32
|
+
|
|
33
|
+
for i, (detection, mask) in enumerate(zip(detections, masks)):
|
|
34
|
+
random_color = random_bright_rgb_0_1()
|
|
35
|
+
|
|
36
|
+
plt.gca().add_patch(
|
|
37
|
+
Rectangle(
|
|
38
|
+
(detection.bbox[0], detection.bbox[1]),
|
|
39
|
+
detection.bbox[2],
|
|
40
|
+
detection.bbox[3],
|
|
41
|
+
fill=False,
|
|
42
|
+
color=random_color,
|
|
43
|
+
)
|
|
44
|
+
)
|
|
45
|
+
plt.text(
|
|
46
|
+
detection.bbox[0] + 8,
|
|
47
|
+
detection.bbox[1] - 20,
|
|
48
|
+
f"Animal {i + 1}: {detection.confidence:.2f}",
|
|
49
|
+
color="white",
|
|
50
|
+
fontsize=12,
|
|
51
|
+
bbox=dict(
|
|
52
|
+
facecolor=random_color,
|
|
53
|
+
alpha=0.5,
|
|
54
|
+
edgecolor="none",
|
|
55
|
+
boxstyle="round,pad=0.3",
|
|
56
|
+
),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Create an overlay for the mask in the same color
|
|
60
|
+
mask_2d = mask.squeeze() # Remove extra dimension
|
|
61
|
+
overlay = np.zeros_like(image, dtype=np.float32)
|
|
62
|
+
overlay[mask_2d] = random_color
|
|
63
|
+
plt.imshow(overlay, alpha=0.5)
|
|
64
|
+
|
|
65
|
+
plt.axis("off")
|
|
66
|
+
plt.show()
|
|
67
|
+
return plt.gcf() # Return the figure object for further manipulation if needed
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def mask_to_bbox(mask):
|
|
71
|
+
"""Convert a mask to a bounding box."""
|
|
72
|
+
|
|
73
|
+
mask_2d = mask.squeeze().numpy().astype("uint8")
|
|
74
|
+
mask_pil = Image.fromarray(mask_2d * 255, mode="L")
|
|
75
|
+
bbox = mask_pil.getbbox()
|
|
76
|
+
return bbox
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def extract_masks(image, masks, whole_image=False):
|
|
80
|
+
"""Extracts the original image filtered by each detected animal mask, with background transparent."""
|
|
81
|
+
|
|
82
|
+
extracted_images = []
|
|
83
|
+
image_np = np.array(image.convert("RGBA"))
|
|
84
|
+
for mask in masks:
|
|
85
|
+
mask_2d = mask.squeeze().numpy().astype("uint8")
|
|
86
|
+
# Create an alpha channel based on the mask
|
|
87
|
+
alpha = (mask_2d * 255).astype("uint8")
|
|
88
|
+
# Stack alpha channel to image
|
|
89
|
+
rgba = image_np.copy()
|
|
90
|
+
rgba[..., 3] = alpha
|
|
91
|
+
filtered_img = Image.fromarray(rgba, mode="RGBA")
|
|
92
|
+
extracted_images.append(filtered_img)
|
|
93
|
+
|
|
94
|
+
if not whole_image:
|
|
95
|
+
# Crop the image to the bounding box of the mask
|
|
96
|
+
bbox = mask_to_bbox(mask)
|
|
97
|
+
filtered_img = filtered_img.crop(bbox)
|
|
98
|
+
extracted_images[-1] = filtered_img
|
|
99
|
+
|
|
100
|
+
return extracted_images
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: segment-animals
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Segment (Extract) Animals from Images - Removing Background
|
|
5
|
+
Author-email: Ben Evans <ben@bluechimp.io>
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Requires-Dist: megadetector>=5.0.29
|
|
8
|
+
Requires-Dist: pillow>=11.2.1
|
|
9
|
+
Requires-Dist: requests>=2.32.4
|
|
10
|
+
Requires-Dist: segment-anything
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# Segment Animals
|
|
14
|
+
|
|
15
|
+
Segment Animals is a Python package for segmenting (extracting) animals from images using deep learning models. It provides a pipeline that combines object detection and segmentation to identify and extract animals from images, making it useful for wildlife research, conservation efforts, and any application where you wish to remove the background from images containing animals.
|
|
16
|
+
|
|
17
|
+
Segment Animals builds upon the [Segment Anything](https://github.com/facebookresearch/segment-anything) and [MegaDetector](https://github.com/agentmorris/MegaDetector/blob/main/getting-started.md) models.
|
|
18
|
+
|
|
19
|
+
# Installation
|
|
20
|
+
|
|
21
|
+
You can install Segment Animals using pip:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install segment-animals
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
Here's a quick example of how to use Segment Animals, for a more detailed guide refer to the [notebook](./notebook.ipynb).
|
|
30
|
+
|
|
31
|
+
### Importing the library and processing an image
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from segment_animals import AutoAnimalSegmenter
|
|
35
|
+
from segment_animals.util import load_image
|
|
36
|
+
|
|
37
|
+
model = AutoAnimalSegmenter()
|
|
38
|
+
|
|
39
|
+
image = load_image("path/to/your/image.jpg")
|
|
40
|
+
|
|
41
|
+
detections, masks = model.process_image(image)
|
|
42
|
+
print(f"Found {len(detections)} animals.")
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Visualizing detections and masks
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from segment_animals.viz import plot_detections_and_masks
|
|
49
|
+
|
|
50
|
+
plot_detections_and_masks(image, detections, masks)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
You should then see a visualisation along the lines of this ([original image from Wikipedia](https://commons.wikimedia.org/wiki/File:Camouflaged_Predator.jpg))...
|
|
54
|
+
|
|
55
|
+

|
|
56
|
+
|
|
57
|
+
### Extracting and saving masks
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from segment_animals.viz import extract_masks
|
|
61
|
+
|
|
62
|
+
# Setting whole_image to False will return individual masks cropped to the extent
|
|
63
|
+
# of the predicted masks.
|
|
64
|
+
for i, mask_extract in enumerate(extract_masks(image, masks, whole_image=False)):
|
|
65
|
+
# mask_extract is a PIL Image object so you can save it or manipulate it further
|
|
66
|
+
mask_extract.save(f"animal_mask_{i}.png")
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Resulting in something like this:
|
|
70
|
+
|
|
71
|
+

|
|
72
|
+
|
|
73
|
+
# Working with Segment Animals?
|
|
74
|
+
|
|
75
|
+
It'd be great to hear how you're using Segment Animals! Drop me a line at Benjamin.Evans at ioz.ac.uk or open an issue on the [GitHub repository](https://github.com/bencevans/segment-animals/issues).
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
segment_animals/__init__.py,sha256=R6bXAh2SQ40zkEKCkkaE27Sai7qOqVgjoz4ggRU8RDw,1686
|
|
2
|
+
segment_animals/detect.py,sha256=pR6xeoYAo6MccWKtxO1N5Nzy232Bb49bZdDwNb2nlP4,1691
|
|
3
|
+
segment_animals/model_cache.py,sha256=QjypUGXD2ncTI7RwrsXlUVta43HDJRXydlewzHQW_XA,7847
|
|
4
|
+
segment_animals/models.py,sha256=N98DcIiYXRad_Vb0hRRRh3rQbIgwdV3zQVC_44MTa3Q,581
|
|
5
|
+
segment_animals/segment.py,sha256=UqdC6SW_4UR6KqK-OBgGDKrmSdOuHFJgRTzVXmSg3CE,2384
|
|
6
|
+
segment_animals/util.py,sha256=v8RC-crFNOUsNFIGkXmOvbFH390xhfApiCLUGx9ToA4,1014
|
|
7
|
+
segment_animals/viz.py,sha256=fzPYARppg9o74b0iZYQ5IaNClb1Jwmxygj66gNEgKzo,3075
|
|
8
|
+
segment_animals-1.0.0.dist-info/METADATA,sha256=TpAHXBxjqLyXZ6zavnLazfkT5PLEuCxj4v2QBXdHA4E,2631
|
|
9
|
+
segment_animals-1.0.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
10
|
+
segment_animals-1.0.0.dist-info/entry_points.txt,sha256=EL5kd7XoXzMszTmmwZKaLmXlEmezkeTWh6kgWRaUArY,57
|
|
11
|
+
segment_animals-1.0.0.dist-info/RECORD,,
|