awc-helpers 0.1.2__tar.gz → 0.1.3__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: awc_helpers
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: Australian Wildlife Conservancy's Wildlife detection and species classification inference tools
5
5
  Author: Quan Tran
6
6
  License: CC-BY-NC-SA-4.0
@@ -51,11 +51,6 @@ pip install torch==2.9.1
51
51
  pip install awc-helpers
52
52
  ```
53
53
 
54
- **From GitHub:**
55
- ```bash
56
- pip install git+https://github.com/Australian-Wildlife-Conservancy-AWC/awc_inference.git
57
- ```
58
-
59
54
  ## Usage
60
55
 
61
56
  ```python
@@ -72,13 +67,19 @@ pipeline = DetectAndClassify(
72
67
 
73
68
  # Run inference on image paths
74
69
  results = pipeline.predict(
75
- inp=["image1.jpg", "image2.jpg"],
70
+ inp=["path/to/image1.jpg", "path/to/image2.jpg"],
76
71
  clas_bs=4
77
72
  )
78
73
 
79
- # Results format: [(identifier, bbox, label, confidence), ...]
74
+ # Results format: [(image_path, bbox_confidence, bbox, label, label_confidence), ...]
80
75
  for result in results:
81
76
  print(result)
77
+ # print example:
78
+ # ("path/to/image1.jpg",
79
+ # 0.804,
80
+ # (0.2246, 0.5885, 0.0678, 0.1022),
81
+ # 'Acanthagenys rufogularis | Spiny-cheeked Honeyeater',
82
+ # 0.9948)
82
83
  ```
83
84
 
84
85
  ## License
@@ -23,11 +23,6 @@ pip install torch==2.9.1
23
23
  pip install awc-helpers
24
24
  ```
25
25
 
26
- **From GitHub:**
27
- ```bash
28
- pip install git+https://github.com/Australian-Wildlife-Conservancy-AWC/awc_inference.git
29
- ```
30
-
31
26
  ## Usage
32
27
 
33
28
  ```python
@@ -44,13 +39,19 @@ pipeline = DetectAndClassify(
44
39
 
45
40
  # Run inference on image paths
46
41
  results = pipeline.predict(
47
- inp=["image1.jpg", "image2.jpg"],
42
+ inp=["path/to/image1.jpg", "path/to/image2.jpg"],
48
43
  clas_bs=4
49
44
  )
50
45
 
51
- # Results format: [(identifier, bbox, label, confidence), ...]
46
+ # Results format: [(image_path, bbox_confidence, bbox, label, label_confidence), ...]
52
47
  for result in results:
53
48
  print(result)
49
+ # print example:
50
+ # ("path/to/image1.jpg",
51
+ # 0.804,
52
+ # (0.2246, 0.5885, 0.0678, 0.1022),
53
+ # 'Acanthagenys rufogularis | Spiny-cheeked Honeyeater',
54
+ # 0.9948)
54
55
  ```
55
56
 
56
57
  ## License
@@ -1,14 +1,22 @@
1
1
  """AWC Helpers - Wildlife detection and classification inference tools."""
2
2
 
3
+ from importlib.metadata import version
4
+
3
5
  from .awc_inference import (
4
6
  DetectAndClassify,
5
7
  SpeciesClasInference,
6
8
  format_md_detections,
7
9
  load_classification_model,
8
10
  )
11
+ from .format_utils import (
12
+ output_csv,
13
+ output_timelapse_json,
14
+ truncate_float,
15
+ truncate_float_array,
16
+ )
9
17
  from .math_utils import crop_image, pil_to_tensor
10
18
 
11
- __version__ = "0.1.2"
19
+ __version__ = version("awc_helpers")
12
20
 
13
21
  __all__ = [
14
22
  "DetectAndClassify",
@@ -17,4 +25,8 @@ __all__ = [
17
25
  "load_classification_model",
18
26
  "crop_image",
19
27
  "pil_to_tensor",
28
+ "output_csv",
29
+ "output_timelapse_json",
30
+ "truncate_float",
31
+ "truncate_float_array",
20
32
  ]
@@ -26,6 +26,7 @@ from megadetector.detection import run_detector
26
26
  from typing import List, Tuple, Union
27
27
  from PIL import Image
28
28
  from .math_utils import crop_image, pil_to_tensor
29
+ from .format_utils import output_csv, output_timelapse_json
29
30
  import logging
30
31
 
31
32
  logger = logging.getLogger(__name__)
@@ -124,7 +125,6 @@ class SpeciesClasInference:
124
125
  model: The loaded classification model.
125
126
  label_names: List of class label names.
126
127
  clas_threshold: Minimum confidence threshold for predictions.
127
- pred_topn: Number of top predictions to return per image.
128
128
  prob_round: Decimal places to round probabilities.
129
129
  use_fp16: Whether to use FP16 mixed precision inference.
130
130
  resize_size: Target size for image resizing before inference.
@@ -136,14 +136,13 @@ class SpeciesClasInference:
136
136
  ... classifier_base='tf_efficientnet_b5.ns_jft_in1k',
137
137
  ... label_names=['cat', 'dog', 'bird']
138
138
  ... )
139
- >>> results = classifier.predict_batch([(image_path, bbox)])
139
+ >>> results = classifier.predict_batch([(image_path, bbox_conf,bbox)])
140
140
  """
141
141
 
142
142
  def __init__(self,
143
143
  classifier_path: str,
144
144
  classifier_base: str,
145
145
  label_names: List[str] = None,
146
- pred_topn: int = 1,
147
146
  prob_round: int = 4,
148
147
  clas_threshold: float = 0.5,
149
148
  resize_size: int = 300,
@@ -167,7 +166,6 @@ class SpeciesClasInference:
167
166
  self.model.eval()
168
167
 
169
168
  self.clas_threshold = clas_threshold
170
- self.pred_topn=pred_topn
171
169
  self.prob_round=prob_round
172
170
  self.use_fp16=use_fp16 and self.device.type=='cuda'
173
171
  self.resize_size=resize_size
@@ -197,7 +195,7 @@ class SpeciesClasInference:
197
195
  img = source.convert('RGB') if source.mode != 'RGB' else source
198
196
  return crop_image(img, bbox_norm, square_crop=True)
199
197
 
200
- def _predict(self, input_tensor: torch.Tensor) -> torch.Tensor:
198
+ def _predict(self, input_tensor: torch.Tensor, pred_topn: int) -> torch.Tensor:
201
199
  """
202
200
  Run classification model on input tensor.
203
201
 
@@ -219,7 +217,7 @@ class SpeciesClasInference:
219
217
  # Softmax in fp32 for numerical stability
220
218
  probs = torch.nn.functional.softmax(logits.float(), dim=1)
221
219
 
222
- top_probs, top_indices = torch.topk(probs, k=self.pred_topn, dim=1)
220
+ top_probs, top_indices = torch.topk(probs, k=pred_topn, dim=1)
223
221
  return (top_probs.cpu().numpy().round(self.prob_round),
224
222
  top_indices.cpu().numpy())
225
223
 
@@ -254,6 +252,7 @@ class SpeciesClasInference:
254
252
  def predict_batch(
255
253
  self,
256
254
  inputs: List[Union[Tuple[str, float, Tuple[float, float, float, float]], Tuple[Image.Image, str, float, Tuple[float, float, float, float]]]],
255
+ pred_topn: int = 1,
257
256
  batch_size: int = 1,
258
257
  ) -> List[Tuple]:
259
258
  """
@@ -262,7 +261,6 @@ class SpeciesClasInference:
262
261
  Args:
263
262
  inputs: List of (img_path, bbox_confidence, bbox) tuples, or (PIL Image, id, bbox_confidence, bbox) tuples for streaming
264
263
  pred_topn: Number of top predictions to return
265
- prob_round: Decimal places to round probabilities
266
264
  batch_size: Number of images to process at once
267
265
 
268
266
  Returns:
@@ -296,7 +294,7 @@ class SpeciesClasInference:
296
294
 
297
295
  # Stack and run inference
298
296
  batch_tensor = torch.cat(batch_tensors, dim=0)
299
- top_probs, top_indices = self._predict(batch_tensor)
297
+ top_probs, top_indices = self._predict(batch_tensor, pred_topn=pred_topn)
300
298
 
301
299
  for i, (identifier, bbox_conf, bbox) in enumerate(batch_metadata):
302
300
  result = self._format_output(
@@ -337,7 +335,6 @@ class DetectAndClassify:
337
335
  classifier_base: str = 'tf_efficientnet_b5.ns_jft_in1k',
338
336
  detection_threshold: float = 0.1,
339
337
  clas_threshold: float = 0.5,
340
- pred_topn: int = 1,
341
338
  resize_size: int = 300,
342
339
  force_cpu: bool = False,
343
340
  skip_clas_errors: bool = True):
@@ -351,7 +348,6 @@ class DetectAndClassify:
351
348
  classifier_base: Name of the base timm model architecture.
352
349
  detection_threshold: Minimum confidence for animal detections.
353
350
  clas_threshold: Minimum confidence for classification predictions.
354
- pred_topn: Number of top classification predictions to return.
355
351
  resize_size: Target image size for classification model input.
356
352
  force_cpu: If True, use CPU even if CUDA is available.
357
353
  skip_clas_errors: If True, skip classification errors instead of raising.
@@ -362,7 +358,6 @@ class DetectAndClassify:
362
358
  classifier_base=classifier_base,
363
359
  clas_threshold=clas_threshold,
364
360
  label_names=label_names,
365
- pred_topn=pred_topn,
366
361
  resize_size=resize_size,
367
362
  force_cpu=force_cpu,
368
363
  skip_errors=skip_clas_errors)
@@ -410,7 +405,9 @@ class DetectAndClassify:
410
405
  self,
411
406
  inp: Union[str, Image.Image, List[Union[str, Image.Image]]],
412
407
  identifier: Union[str, List[str], None] = None,
413
- clas_bs: int = 4
408
+ clas_bs: int = 4,
409
+ topn: int = 1,
410
+ output_name: str = None,
414
411
  ) -> List[Tuple]:
415
412
  """
416
413
  Run detection and classification on input images.
@@ -424,11 +421,14 @@ class DetectAndClassify:
424
421
  identifier: Optional identifier(s) for tracking results back to
425
422
  source images. If None, uses file paths or timestamps.
426
423
  clas_bs: Batch size for classification inference.
427
-
424
+ topn: Number of top classification predictions to return.
425
+ output_name: Optional name for saving results (CSV and Timelapse's JSON) instead of returning it.
428
426
  Returns:
429
427
  List of result tuples, one per detected animal. Each tuple contains:
430
- (identifier, bbox, label1, prob1, label2, prob2, ...) where the
431
- number of label/prob pairs depends on pred_topn and clas_threshold.
428
+ (identifier, bbox_conf, bbox, label1, prob1, label2, prob2, ...) where the
429
+ number of label/prob pairs depends on topn and clas_threshold.
430
+
431
+ If output_name is provided, results are saved to file, no results returned.
432
432
  """
433
433
  inp, identifier = self._validate_input(inp, identifier)
434
434
  if len(inp) == 0:
@@ -438,7 +438,11 @@ class DetectAndClassify:
438
438
  for item,id in zip(inp, identifier):
439
439
  img = item
440
440
  if isinstance(item,str):
441
- img = Image.open(item)
441
+ try:
442
+ img = Image.open(item)
443
+ except Exception as e:
444
+ logger.warning(f"Failed to open image {item}: {e}")
445
+ continue
442
446
  try:
443
447
  md_result = self.md_detector.generate_detections_one_image(img,id,
444
448
  detection_threshold=self.detection_threshold)
@@ -449,4 +453,11 @@ class DetectAndClassify:
449
453
  if isinstance(item,str):
450
454
  img.close()
451
455
 
452
- return self.clas_inference.predict_batch(md_results, batch_size=clas_bs)
456
+ clas_results = self.clas_inference.predict_batch(md_results, pred_topn=topn, batch_size=clas_bs)
457
+ if output_name is None:
458
+ return clas_results
459
+
460
+ output_csv(clas_results, output_name)
461
+ output_timelapse_json(clas_results, output_name, self.clas_inference.label_names)
462
+
463
+
@@ -0,0 +1,149 @@
1
+ import csv
2
+ import json
3
+ import math
4
+ from datetime import datetime
5
+ from typing import List, Tuple, Dict, Any
6
+ from collections import OrderedDict
7
+
8
+
9
+ def truncate_float(x: float, precision: int = 3) -> float:
10
+ """
11
+ Truncates the fractional portion of a floating-point value to a specific number of
12
+ floating-point digits.
13
+ Source: https://github.com/agentmorris/MegaDetector/blob/main/megadetector/utils/ct_utils.py
14
+
15
+ Args:
16
+ x (float): scalar to truncate
17
+ precision (int, optional): the number of significant digits to preserve, should be >= 1
18
+
19
+ Returns:
20
+ float: truncated version of [x]
21
+ """
22
+ return math.floor(x * (10 ** precision)) / (10 ** precision)
23
+
24
+
25
+ def truncate_float_array(arr: List[float], precision: int = 4) -> List[float]:
26
+ return [truncate_float(x, precision) for x in arr]
27
+
28
+
29
+ def output_timelapse_json(clas_results: List[Tuple], json_name: str, label_names: List[str]):
30
+ """
31
+ Convert classification results to timelapse JSON format.
32
+
33
+ Args:
34
+ clas_results: List of result tuples, one per detected animal. Each tuple contains:
35
+ (identifier, bbox_conf, bbox, label1, prob1, label2, prob2, ...) where the
36
+ number of label/prob pairs depends on pred_topn and clas_threshold.
37
+ json_name: Output JSON file name.
38
+ label_names: List of all label names.
39
+ """
40
+ if not json_name.endswith('.json'):
41
+ json_name += '.json'
42
+
43
+ # Group detections by file using OrderedDict to preserve order
44
+ images_dict: Dict[str, List[Dict[str, Any]]] = OrderedDict()
45
+
46
+ for result in clas_results:
47
+ identifier = result[0]
48
+ bbox_conf = result[1]
49
+ bbox = result[2]
50
+
51
+ # Initialize file entry if not exists
52
+ if identifier not in images_dict:
53
+ images_dict[identifier] = []
54
+
55
+ # If bbox is None or empty, this image has no detections
56
+ if bbox is None or bbox_conf is None:
57
+ continue
58
+
59
+ # Build detection object
60
+ detection = {
61
+ "category": "1", # Always "1" for animal
62
+ "conf": truncate_float(bbox_conf, precision=3),
63
+ "bbox": truncate_float_array(list(bbox), precision=4)
64
+ }
65
+
66
+ clas2idx = {name: str(i + 1) for i, name in enumerate(label_names)}
67
+
68
+ classifications = []
69
+ for i in range(3, len(result), 2):
70
+ if i + 1 < len(result):
71
+ label_str = result[i]
72
+ prob = result[i + 1]
73
+ if label_str is not None and prob is not None:
74
+ classifications.append([clas2idx[label_str], truncate_float(prob, precision=3)])
75
+
76
+ if classifications:
77
+ detection["classifications"] = classifications
78
+
79
+ images_dict[identifier].append(detection)
80
+
81
+ # Build images list
82
+ images = []
83
+ for file_path, detections in images_dict.items():
84
+ images.append({
85
+ "file": file_path,
86
+ "detections": detections
87
+ })
88
+
89
+ idx2clas = {str(i + 1): name for i, name in enumerate(label_names)}
90
+ current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
91
+ # Build output structure
92
+ output = {
93
+ "images": images,
94
+ "detection_categories": {
95
+ "1": "animal",
96
+ "2": "person",
97
+ "3": "vehicle"
98
+ },
99
+ "info": {
100
+ "detection_completion_time": current_time,
101
+ "format_version": "1.4",
102
+ "detector": "md_v1000.0.0-redwood.pt",
103
+ "detector_metadata": {
104
+ "megadetector_version": "1000-redwood"
105
+ },
106
+ "python_library": "awc-helpers"
107
+ },
108
+ "classification_categories": idx2clas
109
+ }
110
+
111
+ # Write to file
112
+ with open(json_name, 'w') as f:
113
+ json.dump(output, f, indent=1)
114
+
115
+ def output_csv(clas_results: List[Tuple],csv_name: str):
116
+ """
117
+ Convert classification results to CSV format.
118
+ Args:
119
+ clas_results: List of result tuples, one per detected animal. Each tuple contains:
120
+ (identifier, bbox_conf, bbox, label1, prob1, label2, prob2, ...) where the
121
+ number of label/prob pairs depends on pred_topn and clas_threshold.
122
+ csv_name: Output CSV file name.
123
+ """
124
+ if not csv_name.endswith('.csv'):
125
+ csv_name += '.csv'
126
+
127
+ # Determine the maximum number of label/prob pairs
128
+ max_pairs = 0
129
+ for result in clas_results:
130
+ num_pairs = (len(result) - 3) // 2
131
+ if num_pairs > max_pairs:
132
+ max_pairs = num_pairs
133
+
134
+ # Create CSV header
135
+ header = ['Image Path', 'Bounding Box Confidence', 'Bounding Box Normalized']
136
+ for i in range(1, max_pairs + 1):
137
+ header.append(f'Label {i}')
138
+ header.append(f'Confidence {i}')
139
+
140
+ # Write to CSV
141
+ with open(csv_name, mode='w', newline='') as csv_file:
142
+ writer = csv.writer(csv_file)
143
+ writer.writerow(header)
144
+ for result in clas_results:
145
+ row = list(result)
146
+ # Pad the row with empty strings if necessary
147
+ while len(row) < 3 + 2 * max_pairs:
148
+ row.append('')
149
+ writer.writerow(row)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: awc-helpers
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: Australian Wildlife Conservancy's Wildlife detection and species classification inference tools
5
5
  Author: Quan Tran
6
6
  License: CC-BY-NC-SA-4.0
@@ -51,11 +51,6 @@ pip install torch==2.9.1
51
51
  pip install awc-helpers
52
52
  ```
53
53
 
54
- **From GitHub:**
55
- ```bash
56
- pip install git+https://github.com/Australian-Wildlife-Conservancy-AWC/awc_inference.git
57
- ```
58
-
59
54
  ## Usage
60
55
 
61
56
  ```python
@@ -72,13 +67,19 @@ pipeline = DetectAndClassify(
72
67
 
73
68
  # Run inference on image paths
74
69
  results = pipeline.predict(
75
- inp=["image1.jpg", "image2.jpg"],
70
+ inp=["path/to/image1.jpg", "path/to/image2.jpg"],
76
71
  clas_bs=4
77
72
  )
78
73
 
79
- # Results format: [(identifier, bbox, label, confidence), ...]
74
+ # Results format: [(image_path, bbox_confidence, bbox, label, label_confidence), ...]
80
75
  for result in results:
81
76
  print(result)
77
+ # print example:
78
+ # ("path/to/image1.jpg",
79
+ # 0.804,
80
+ # (0.2246, 0.5885, 0.0678, 0.1022),
81
+ # 'Acanthagenys rufogularis | Spiny-cheeked Honeyeater',
82
+ # 0.9948)
82
83
  ```
83
84
 
84
85
  ## License
@@ -4,6 +4,7 @@ README.md
4
4
  pyproject.toml
5
5
  awc_helpers/__init__.py
6
6
  awc_helpers/awc_inference.py
7
+ awc_helpers/format_utils.py
7
8
  awc_helpers/math_utils.py
8
9
  awc_helpers.egg-info/PKG-INFO
9
10
  awc_helpers.egg-info/SOURCES.txt
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "awc_helpers"
7
- version = "0.1.2"
7
+ version = "0.1.3"
8
8
  description = "Australian Wildlife Conservancy's Wildlife detection and species classification inference tools"
9
9
  readme = "README.md"
10
10
  license = {text = "CC-BY-NC-SA-4.0"}
File without changes
File without changes
File without changes