Computer-Vision-Assistant-Package 1.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.
- Computer_Vision_Assistant_Package/AI_Metrics.py +241 -0
- Computer_Vision_Assistant_Package/Basic_Utils.py +597 -0
- Computer_Vision_Assistant_Package/Image_Utils.py +265 -0
- Computer_Vision_Assistant_Package/__innit__.py +21 -0
- computer_vision_assistant_package-1.0.dist-info/METADATA +151 -0
- computer_vision_assistant_package-1.0.dist-info/RECORD +8 -0
- computer_vision_assistant_package-1.0.dist-info/WHEEL +4 -0
- computer_vision_assistant_package-1.0.dist-info/licenses/License.txt +21 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"""Detection metrics for object detectors.
|
|
2
|
+
|
|
3
|
+
Functions to score how well a detector's boxes match the ground-truth boxes:
|
|
4
|
+
IoU, precision / recall and mAP. All boxes are in
|
|
5
|
+
``(x_min, y_min, x_max, y_max)`` pixel format.
|
|
6
|
+
|
|
7
|
+
The overall flow is::
|
|
8
|
+
|
|
9
|
+
detection_metrics
|
|
10
|
+
|-- provide_annotations() (Basic_Utils) -> ground-truth boxes
|
|
11
|
+
|-- ai_analytics() (Basic_Utils) -> predicted boxes
|
|
12
|
+
|-- IOU_calc() -> IoU of each prediction vs the GT boxes
|
|
13
|
+
|-- mAP_calc() -> precision / recall at each IoU threshold
|
|
14
|
+
`-- AUC_calc() -> smoothed precision curve for the area
|
|
15
|
+
|
|
16
|
+
Functions
|
|
17
|
+
---------
|
|
18
|
+
IOU_calc
|
|
19
|
+
Intersection-over-Union of one box against a list of boxes.
|
|
20
|
+
mAP_calc
|
|
21
|
+
Precision and recall lists for IoU thresholds 0.50 to 0.90.
|
|
22
|
+
AUC_calc
|
|
23
|
+
Builds the (recall, smoothed precision) curve used to compute the area.
|
|
24
|
+
detection_metrics
|
|
25
|
+
End-to-end mAP@0.5 and mAP@0.5:0.95 for one image.
|
|
26
|
+
|
|
27
|
+
Author: Shounak Sanyal
|
|
28
|
+
"""
|
|
29
|
+
import os, json, cv2
|
|
30
|
+
import numpy as np
|
|
31
|
+
from .Basic_Utils import *
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def IOU_calc(target_pts, gt_pts):
|
|
35
|
+
"""Compute the IoU of one predicted box against every ground-truth box.
|
|
36
|
+
|
|
37
|
+
IoU (Intersection over Union) tells how much two boxes overlap:
|
|
38
|
+
0 means no overlap and 1 means the boxes are identical.
|
|
39
|
+
|
|
40
|
+
Parameters
|
|
41
|
+
----------
|
|
42
|
+
target_pts : sequence of 4 numbers
|
|
43
|
+
The predicted box as ``(x_min, y_min, x_max, y_max)``.
|
|
44
|
+
gt_pts : list of sequence of 4 numbers
|
|
45
|
+
Ground-truth boxes, each as ``(x_min, y_min, x_max, y_max)``.
|
|
46
|
+
|
|
47
|
+
Returns
|
|
48
|
+
-------
|
|
49
|
+
IOU_arr : list of float
|
|
50
|
+
One IoU value (0 to 1) per ground-truth box, in the same order as
|
|
51
|
+
``gt_pts``.
|
|
52
|
+
|
|
53
|
+
Examples
|
|
54
|
+
--------
|
|
55
|
+
>>> IOU_calc((0, 0, 10, 10), [(5, 5, 15, 15), (0, 0, 10, 10)])
|
|
56
|
+
[0.14285714285714285, 1.0]
|
|
57
|
+
"""
|
|
58
|
+
IOU_arr=[]
|
|
59
|
+
for i in range(0, len(gt_pts)):
|
|
60
|
+
x_gt0, y_gt0,x_gt1, y_gt1=gt_pts[i]
|
|
61
|
+
x_tgt0, y_tgt0, x_tgt1, y_tgt1= target_pts
|
|
62
|
+
w_gt, h_gt, w_tgt, h_tgt= x_gt1- x_gt0, y_gt1- y_gt0, x_tgt1- x_tgt0, y_tgt1- y_tgt0
|
|
63
|
+
x_edge, y_edge= min(x_gt1, x_tgt1), min(y_gt1, y_tgt1)
|
|
64
|
+
x_ref, y_ref= max(x_gt0, x_tgt0), max(y_gt0, y_tgt0)
|
|
65
|
+
|
|
66
|
+
if (x_ref>=x_edge) or (y_ref>= y_edge):
|
|
67
|
+
Intersection=0
|
|
68
|
+
else:
|
|
69
|
+
w,h= x_edge-x_ref, y_edge-y_ref
|
|
70
|
+
Intersection= h*w
|
|
71
|
+
Union= (h_gt*w_gt)+ (h_tgt*w_tgt)- Intersection
|
|
72
|
+
IOU= Intersection/Union
|
|
73
|
+
|
|
74
|
+
IOU_arr.append(IOU)
|
|
75
|
+
|
|
76
|
+
return IOU_arr
|
|
77
|
+
|
|
78
|
+
def mAP_calc(bboxes, gt_bboxes):
|
|
79
|
+
"""Compute running precision and recall for IoU thresholds 0.50 to 0.90.
|
|
80
|
+
|
|
81
|
+
The predictions must be sorted by confidence (highest first). For each IoU
|
|
82
|
+
threshold (0.50, 0.55, ..., 0.90) the predictions are visited in order. A
|
|
83
|
+
prediction counts as a true positive if its IoU reaches the threshold and its
|
|
84
|
+
matched ground-truth box has not been used yet; otherwise it is a false
|
|
85
|
+
positive. Precision and recall are recorded after every prediction.
|
|
86
|
+
|
|
87
|
+
Parameters
|
|
88
|
+
----------
|
|
89
|
+
bboxes : array-like of float
|
|
90
|
+
The IoU value of each prediction, sorted by descending confidence.
|
|
91
|
+
gt_bboxes : array-like of int
|
|
92
|
+
For each prediction, the index of the ground-truth box it overlaps
|
|
93
|
+
most (same length and order as ``bboxes``).
|
|
94
|
+
|
|
95
|
+
Returns
|
|
96
|
+
-------
|
|
97
|
+
net_precision_arr : list of list of float
|
|
98
|
+
One list per threshold; each holds the precision after every
|
|
99
|
+
prediction.
|
|
100
|
+
net_recall_arr : list of list of float
|
|
101
|
+
Same layout as ``net_precision_arr``, but for recall.
|
|
102
|
+
"""
|
|
103
|
+
thresh_limits= np.arange(0.5, 1, 0.05)
|
|
104
|
+
net_precision_arr, net_recall_arr=[],[]
|
|
105
|
+
for index in range(0, len(thresh_limits)):
|
|
106
|
+
net_positive= len(np.unique(gt_bboxes))
|
|
107
|
+
true_positive, false_positive=0,0
|
|
108
|
+
precision_arr, recall_arr=[],[]
|
|
109
|
+
checked_gt=[]
|
|
110
|
+
for i in range(0, len(bboxes)):
|
|
111
|
+
if bboxes[i]< thresh_limits[index]:
|
|
112
|
+
|
|
113
|
+
false_positive+=1
|
|
114
|
+
else:
|
|
115
|
+
if gt_bboxes[i] not in checked_gt:
|
|
116
|
+
true_positive+=1
|
|
117
|
+
checked_gt.append(gt_bboxes[i])
|
|
118
|
+
else:
|
|
119
|
+
false_positive+=1
|
|
120
|
+
|
|
121
|
+
precision= true_positive/(true_positive+false_positive)
|
|
122
|
+
recall= true_positive/net_positive
|
|
123
|
+
precision_arr.append(precision)
|
|
124
|
+
recall_arr.append(recall)
|
|
125
|
+
net_precision_arr.append(precision_arr)
|
|
126
|
+
net_recall_arr.append(recall_arr)
|
|
127
|
+
|
|
128
|
+
return net_precision_arr, net_recall_arr
|
|
129
|
+
|
|
130
|
+
def AUC_calc(precision, recall):
|
|
131
|
+
"""Build the smoothed precision-recall curve used to get the area.
|
|
132
|
+
|
|
133
|
+
Each precision value is replaced by the highest precision found at or after
|
|
134
|
+
that point, so the curve never goes up as recall increases. The recall values
|
|
135
|
+
are returned unchanged.
|
|
136
|
+
|
|
137
|
+
Parameters
|
|
138
|
+
----------
|
|
139
|
+
precision : sequence of float
|
|
140
|
+
Precision values in prediction order (one list from ``mAP_calc``).
|
|
141
|
+
recall : sequence of float
|
|
142
|
+
Recall values in the same order.
|
|
143
|
+
|
|
144
|
+
Returns
|
|
145
|
+
-------
|
|
146
|
+
x_arr : sequence of float
|
|
147
|
+
The recall values.
|
|
148
|
+
y_arr : list of float
|
|
149
|
+
The smoothed precision values, same length as ``precision``.
|
|
150
|
+
"""
|
|
151
|
+
x_arr, y_arr=[],[]
|
|
152
|
+
for i in range(0, len(precision)):
|
|
153
|
+
if precision[i]> np.max(precision[i:]):
|
|
154
|
+
y_arr.append(precision[i])
|
|
155
|
+
else:
|
|
156
|
+
y_arr.append(np.max(precision[i:]))
|
|
157
|
+
x_arr= recall
|
|
158
|
+
|
|
159
|
+
return x_arr, y_arr
|
|
160
|
+
|
|
161
|
+
def detection_metrics(json_file, img_file, model_test):
|
|
162
|
+
"""Evaluate a detector on one image and return mAP@0.5 and mAP@0.5:0.95.
|
|
163
|
+
|
|
164
|
+
Steps: load the ground truth from a COCO json, run the model, match every
|
|
165
|
+
prediction to its best-overlapping ground-truth box, sort by confidence,
|
|
166
|
+
compute precision / recall for each IoU threshold and integrate the area.
|
|
167
|
+
|
|
168
|
+
Parameters
|
|
169
|
+
----------
|
|
170
|
+
json_file : str
|
|
171
|
+
Path to the COCO-format annotation file that lists ``img_file``.
|
|
172
|
+
img_file : str
|
|
173
|
+
Path to the image to evaluate.
|
|
174
|
+
model_test : object
|
|
175
|
+
Loaded detection model (RF-DETR style) with a ``.predict(image_path)``
|
|
176
|
+
method that returns boxes, labels and confidence scores.
|
|
177
|
+
|
|
178
|
+
Returns
|
|
179
|
+
-------
|
|
180
|
+
mAP_50 : float
|
|
181
|
+
Score at IoU threshold 0.50, rounded to 3 decimals.
|
|
182
|
+
mAP_50_95 : float
|
|
183
|
+
Score averaged over the IoU thresholds, rounded to 3 decimals.
|
|
184
|
+
|
|
185
|
+
Examples
|
|
186
|
+
--------
|
|
187
|
+
>>> mAP_50, mAP_50_95 = detection_metrics("annotations.json", "img_01.png", model)
|
|
188
|
+
"""
|
|
189
|
+
gt_pts, gt_indices=[],[]
|
|
190
|
+
gt_coords, gt_labels=provide_annotations(json_file, img_file)
|
|
191
|
+
|
|
192
|
+
for j in range(0, len(gt_coords)):
|
|
193
|
+
x0, y0 = int(gt_coords[j][0]), int(gt_coords[j][1])
|
|
194
|
+
x1, y1 = int(gt_coords[j][2])+ x0, int(gt_coords[j][3])+ y0
|
|
195
|
+
gt_pts.append((x0,y0, x1,y1))
|
|
196
|
+
gt_indices.append(j)
|
|
197
|
+
|
|
198
|
+
IOU_arr=[]
|
|
199
|
+
IOU_checkpoints_gt=[]
|
|
200
|
+
|
|
201
|
+
_, coords, labels, _,confidence= ai_analytics(None, img_file, model_test)
|
|
202
|
+
for j in range(0, len(coords)):
|
|
203
|
+
x0, y0 = int(coords[j][0]), int(coords[j][1])
|
|
204
|
+
x1, y1 = int(coords[j][2]), int(coords[j][3])
|
|
205
|
+
tgt_pt=(x0,y0 ,x1,y1)
|
|
206
|
+
IOU=IOU_calc(tgt_pt, gt_pts)
|
|
207
|
+
IOU_checkpoints_gt.append(gt_indices[np.argmax(IOU)])
|
|
208
|
+
IOU=round(np.max(IOU),2)
|
|
209
|
+
IOU_arr.append(IOU)
|
|
210
|
+
|
|
211
|
+
confidence_arr= np.array(confidence)
|
|
212
|
+
IOU_arr= np.array(IOU_arr)
|
|
213
|
+
IOU_checkpoints_gt= np.array(IOU_checkpoints_gt)
|
|
214
|
+
index= np.argsort(confidence_arr)[::-1]
|
|
215
|
+
IOU_arr= IOU_arr[index]
|
|
216
|
+
IOU_checkpoints_gt= IOU_checkpoints_gt[index]
|
|
217
|
+
|
|
218
|
+
net_precision, net_recall= mAP_calc(IOU_arr, IOU_checkpoints_gt)
|
|
219
|
+
net_area=0
|
|
220
|
+
for j in range(0, len(net_precision)):
|
|
221
|
+
x,y= AUC_calc(net_precision[j], net_recall[j])
|
|
222
|
+
|
|
223
|
+
area_arr=[]
|
|
224
|
+
area_i=0
|
|
225
|
+
for k in range(0, len(x)):
|
|
226
|
+
if k<len(x)-1:
|
|
227
|
+
w= x[k+1]-x[k]
|
|
228
|
+
else:
|
|
229
|
+
w=0
|
|
230
|
+
h= y[k]
|
|
231
|
+
area=(h*w)+ area_i
|
|
232
|
+
area_arr.append(area)
|
|
233
|
+
area_i= area_arr[-1]
|
|
234
|
+
net_area+=area_i
|
|
235
|
+
if j==0:
|
|
236
|
+
mAP_50= area_i
|
|
237
|
+
mAP_50_95= net_area/10
|
|
238
|
+
mAP_50, mAP_50_95= round(mAP_50,3), round(mAP_50_95,3)
|
|
239
|
+
|
|
240
|
+
print('net mAP_50', mAP_50, 'net mAP_50:95', mAP_50_95)
|
|
241
|
+
return mAP_50, mAP_50_95
|
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
"""Core dataset, annotation, inference and SAHI-tiling helpers.
|
|
2
|
+
|
|
3
|
+
This is the main working module of the package. It covers four jobs:
|
|
4
|
+
|
|
5
|
+
1. **File discovery**: find images, ``.json`` and ``.txt`` files in a folder
|
|
6
|
+
tree (``img_loc``).
|
|
7
|
+
2. **Dataset checks and annotations**: validate a COCO dataset folder
|
|
8
|
+
(``dataset_check``) and fetch the ground-truth boxes for one image
|
|
9
|
+
(``provide_annotations``).
|
|
10
|
+
3. **Inference**: run a detector or classifier on one image and optionally
|
|
11
|
+
draw the predictions next to the ground truth (``ai_analytics``).
|
|
12
|
+
4. **SAHI tiling**: cut a large image into tiles and run detection (and
|
|
13
|
+
optional classification) on every tile (``SAHI_generation``,
|
|
14
|
+
``SAHI_analysis``).
|
|
15
|
+
|
|
16
|
+
Functions
|
|
17
|
+
---------
|
|
18
|
+
img_loc
|
|
19
|
+
Recursively list image, json and txt files.
|
|
20
|
+
ai_analytics
|
|
21
|
+
Run a model on one image and return its predictions.
|
|
22
|
+
dataset_check
|
|
23
|
+
Sanity-check a folder of images plus a COCO json.
|
|
24
|
+
provide_annotations
|
|
25
|
+
Get the ground-truth boxes and labels for one image.
|
|
26
|
+
SAHI_generation
|
|
27
|
+
Slice a large image into tiles.
|
|
28
|
+
SAHI_analysis
|
|
29
|
+
Run detection / classification on tiles and draw the results on the
|
|
30
|
+
full image.
|
|
31
|
+
|
|
32
|
+
Module Constants
|
|
33
|
+
----------------
|
|
34
|
+
IMAGE_EXTENSIONS : tuple of str
|
|
35
|
+
File extensions treated as images (matched case-insensitively).
|
|
36
|
+
|
|
37
|
+
Requires
|
|
38
|
+
--------
|
|
39
|
+
``numpy``, ``opencv-python``, ``torch``, ``torchvision``, ``matplotlib`` and
|
|
40
|
+
``sahi``. Uses ``cell_geometry`` and ``cell_transform`` from ``Image_Utils``.
|
|
41
|
+
|
|
42
|
+
Author: Shounak Sanyal
|
|
43
|
+
"""
|
|
44
|
+
import os, cv2, json, torch, shutil
|
|
45
|
+
import numpy as np
|
|
46
|
+
from sahi.slicing import slice_image
|
|
47
|
+
import matplotlib.pyplot as plt # currently unused in this module
|
|
48
|
+
from torchvision.transforms import v2
|
|
49
|
+
from torchvision import datasets, models, transforms
|
|
50
|
+
from .Image_Utils import *
|
|
51
|
+
|
|
52
|
+
# Common image file extensions to recognize when scanning directories.
|
|
53
|
+
# Add/remove extensions here if your office image sources use other formats.
|
|
54
|
+
# Matching is case-insensitive (names are lowercased before comparing).
|
|
55
|
+
IMAGE_EXTENSIONS = ('png', 'jpg', 'jpeg', 'bmp', 'tif', 'tiff', 'webp')
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def img_loc(path):
|
|
60
|
+
"""Recursively collect image, json and txt file paths under a folder.
|
|
61
|
+
|
|
62
|
+
Parameters
|
|
63
|
+
----------
|
|
64
|
+
path : str
|
|
65
|
+
Root directory to search. All sub-folders are searched too.
|
|
66
|
+
|
|
67
|
+
Returns
|
|
68
|
+
-------
|
|
69
|
+
img_list : list of str
|
|
70
|
+
Paths of all image files (extensions from ``IMAGE_EXTENSIONS``).
|
|
71
|
+
json_list : list of str
|
|
72
|
+
Paths of all ``.json`` files.
|
|
73
|
+
txt_list : list of str
|
|
74
|
+
Paths of all ``.txt`` files.
|
|
75
|
+
|
|
76
|
+
Examples
|
|
77
|
+
--------
|
|
78
|
+
>>> images, jsons, txts = img_loc("my_dataset")
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
global img_list
|
|
82
|
+
global json_list
|
|
83
|
+
global txt_list
|
|
84
|
+
|
|
85
|
+
img_list=[]
|
|
86
|
+
json_list=[]
|
|
87
|
+
txt_list=[]
|
|
88
|
+
|
|
89
|
+
def find_all_images(path):
|
|
90
|
+
"""Append every image file under ``path`` to the global
|
|
91
|
+
``img_list``, entering each sub-folder recursively."""
|
|
92
|
+
global img_list
|
|
93
|
+
list_path= os.listdir(path)
|
|
94
|
+
for i in range(0, len(list_path)):
|
|
95
|
+
new_path= os.path.join(path, list_path[i])
|
|
96
|
+
if os.path.isfile(new_path):
|
|
97
|
+
if new_path.lower().endswith(IMAGE_EXTENSIONS):
|
|
98
|
+
img_list.append(new_path)
|
|
99
|
+
if os.path.isdir(new_path):
|
|
100
|
+
find_all_images(new_path)
|
|
101
|
+
|
|
102
|
+
def find_all_json(path):
|
|
103
|
+
"""Append every path ending in ``'json'`` under ``path`` to the
|
|
104
|
+
global ``json_list``, entering each sub-folder recursively."""
|
|
105
|
+
global json_list
|
|
106
|
+
list_path= os.listdir(path)
|
|
107
|
+
for i in range(0, len(list_path)):
|
|
108
|
+
new_path= os.path.join(path, list_path[i])
|
|
109
|
+
|
|
110
|
+
if new_path.endswith('json'):
|
|
111
|
+
json_list.append(new_path)
|
|
112
|
+
if os.path.isdir(new_path):
|
|
113
|
+
find_all_json(new_path)
|
|
114
|
+
|
|
115
|
+
def find_all_txt(path):
|
|
116
|
+
"""Append every file ending in ``'txt'`` under ``path`` to the
|
|
117
|
+
global ``txt_list``, entering each sub-folder recursively."""
|
|
118
|
+
global txt_list
|
|
119
|
+
list_path= os.listdir(path)
|
|
120
|
+
for i in range(0, len(list_path)):
|
|
121
|
+
new_path= os.path.join(path, list_path[i])
|
|
122
|
+
if os.path.isfile(new_path):
|
|
123
|
+
if new_path.lower().endswith('txt'):
|
|
124
|
+
txt_list.append(new_path)
|
|
125
|
+
if os.path.isdir(new_path):
|
|
126
|
+
find_all_txt(new_path)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
find_all_images(path)
|
|
130
|
+
find_all_json(path)
|
|
131
|
+
find_all_txt(path)
|
|
132
|
+
return img_list, json_list, txt_list
|
|
133
|
+
|
|
134
|
+
def ai_analytics(file, image_path, model, name='RFDETR', key=True, mode='Detection'):
|
|
135
|
+
"""Run a model on one image and return its predictions.
|
|
136
|
+
|
|
137
|
+
For detection, the ground-truth boxes are also looked up in a COCO json so
|
|
138
|
+
the predictions can be compared with them (and optionally drawn on the
|
|
139
|
+
image).
|
|
140
|
+
|
|
141
|
+
Supported ``(name, mode)`` combinations:
|
|
142
|
+
|
|
143
|
+
* ``('RFDETR', 'Detection')``: ``model.predict(image_path)``
|
|
144
|
+
* ``('YOLO', 'Detection')``: ``model.predict(image_path)``
|
|
145
|
+
* ``('CNN', 'Classification')``: ``model(img)``, where ``img`` is a tensor
|
|
146
|
+
|
|
147
|
+
Parameters
|
|
148
|
+
----------
|
|
149
|
+
file : str or None
|
|
150
|
+
Path to a COCO-format JSON annotation file. If it is ``None`` or cannot
|
|
151
|
+
be read, the function continues without ground truth. Use ``None`` for
|
|
152
|
+
classification.
|
|
153
|
+
image_path : str or torch.Tensor
|
|
154
|
+
Detection: path to the image. Classification: the image tensor itself.
|
|
155
|
+
model : object
|
|
156
|
+
The loaded model (moved to the GPU if available, else the CPU). For
|
|
157
|
+
detection it must have ``.predict()``.
|
|
158
|
+
name : {'RFDETR', 'YOLO', 'CNN'}, optional
|
|
159
|
+
Model type; decides how the output is read. Default is ``'RFDETR'``.
|
|
160
|
+
key : bool, optional
|
|
161
|
+
If True and the image has ground-truth annotations, draw the predicted
|
|
162
|
+
boxes (blue) and the ground-truth boxes (green) on the returned image.
|
|
163
|
+
Default is True.
|
|
164
|
+
mode : {'Detection', 'Classification'}, optional
|
|
165
|
+
Task type. Default is ``'Detection'``.
|
|
166
|
+
|
|
167
|
+
Returns
|
|
168
|
+
-------
|
|
169
|
+
Classification (``'CNN'``):
|
|
170
|
+
pred : numpy.int64
|
|
171
|
+
Index of the class with the highest score.
|
|
172
|
+
results : numpy.ndarray
|
|
173
|
+
Raw model scores for the first item in the batch.
|
|
174
|
+
Detection:
|
|
175
|
+
n_gt : int
|
|
176
|
+
Number of ground-truth boxes found for this image.
|
|
177
|
+
coords : array-like
|
|
178
|
+
Predicted boxes in ``xyxy`` format.
|
|
179
|
+
labels : array-like
|
|
180
|
+
Predicted labels (RFDETR: class names, YOLO: numeric class ids).
|
|
181
|
+
img : numpy.ndarray
|
|
182
|
+
The image (BGR), with the boxes drawn on it when ``key=True`` and
|
|
183
|
+
ground truth was found.
|
|
184
|
+
confidence : array-like
|
|
185
|
+
Confidence score of each prediction.
|
|
186
|
+
|
|
187
|
+
Examples
|
|
188
|
+
--------
|
|
189
|
+
>>> n_gt, coords, labels, img, conf = ai_analytics("annotations.json", "img_01.png", model)
|
|
190
|
+
>>> pred, scores = ai_analytics(None, tensor, cnn_model, name="CNN", mode="Classification")
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
device= "cuda" if torch.cuda.is_available() else "cpu"
|
|
194
|
+
|
|
195
|
+
def find_annots(id, file):
|
|
196
|
+
"""Return all bboxes (``[x, y, w, h]``) and category ids from the
|
|
197
|
+
loaded COCO dict ``file`` whose annotations belong to the image id
|
|
198
|
+
``id``."""
|
|
199
|
+
bbox_list, labels_list=[],[]
|
|
200
|
+
for i in range(0, len(file['annotations'])):
|
|
201
|
+
annot_id= file['annotations'][i]['image_id']
|
|
202
|
+
if annot_id == id:
|
|
203
|
+
bbox_list.append(file['annotations'][i]['bbox'])
|
|
204
|
+
labels_list.append(file['annotations'][i]['category_id'])
|
|
205
|
+
|
|
206
|
+
return bbox_list, labels_list
|
|
207
|
+
|
|
208
|
+
def find_img_id(id, file):
|
|
209
|
+
"""Look up the COCO image id of the image file name ``id`` in the
|
|
210
|
+
loaded COCO dict ``file`` and return its ground truth as
|
|
211
|
+
``(boxes, category ids)`` via ``find_annots``. Prints a message if
|
|
212
|
+
the image has no annotations or is not listed in the json."""
|
|
213
|
+
coords, labels=[],[]
|
|
214
|
+
for i in range(0, len(file['images'])):
|
|
215
|
+
if id== file['images'][i]['file_name']:
|
|
216
|
+
img_id= file['images'][i]['id']
|
|
217
|
+
coords, labels= find_annots(img_id, file)
|
|
218
|
+
if len(coords)==0:
|
|
219
|
+
print('The image', id, 'has no listed annotations')
|
|
220
|
+
break
|
|
221
|
+
if len(coords)==0:
|
|
222
|
+
# Note: this message also shows when the image IS listed but
|
|
223
|
+
# has no annotations (the message above is printed as well).
|
|
224
|
+
print('The image',id, 'is not listed in the json')
|
|
225
|
+
return coords, labels
|
|
226
|
+
|
|
227
|
+
# Run model inference on the target image.
|
|
228
|
+
if mode=='Detection':
|
|
229
|
+
results= model.predict(image_path)
|
|
230
|
+
|
|
231
|
+
if mode=='Classification':
|
|
232
|
+
# If image_path is a file path, read it with cv2; if that fails
|
|
233
|
+
# (e.g. it is already a tensor), use it as it is.
|
|
234
|
+
try:
|
|
235
|
+
img= cv2.imread(image_path)
|
|
236
|
+
except:
|
|
237
|
+
img= image_path
|
|
238
|
+
img= img.to(device)
|
|
239
|
+
results= model(img)
|
|
240
|
+
|
|
241
|
+
# The image file name (last part of the path) is used to find the
|
|
242
|
+
# matching entry in the COCO json. Skipped if image_path is not a string.
|
|
243
|
+
try:
|
|
244
|
+
img_title= os.path.basename(image_path)
|
|
245
|
+
except:
|
|
246
|
+
pass
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
# Attempt to load ground-truth annotations for this image; if the
|
|
250
|
+
# annotation file is missing/invalid, proceed with empty ground truth.
|
|
251
|
+
# (Note: 'file' is re-used here to hold the loaded json data.)
|
|
252
|
+
try:
|
|
253
|
+
with open(file, 'r') as f:
|
|
254
|
+
file= json.load(f)
|
|
255
|
+
gt_annots, gt_labels= find_img_id(img_title,file)
|
|
256
|
+
except:
|
|
257
|
+
print('No annotation files, proceeding without it')
|
|
258
|
+
gt_annots, gt_labels=[],[]
|
|
259
|
+
|
|
260
|
+
confidence=0
|
|
261
|
+
# Read the model output according to the model type.
|
|
262
|
+
if name=='RFDETR' and mode=='Detection':
|
|
263
|
+
# RFDETR (supervision-style Detections): boxes in .xyxy,
|
|
264
|
+
# class names in .data['class_name'].
|
|
265
|
+
coords= results.xyxy
|
|
266
|
+
labels= results.data['class_name']
|
|
267
|
+
confidence= results.confidence
|
|
268
|
+
img= cv2.imread(image_path)
|
|
269
|
+
|
|
270
|
+
if name=='YOLO' and mode=='Detection':
|
|
271
|
+
# YOLO (ultralytics-style Results): boxes in .boxes.xyxy,
|
|
272
|
+
# numeric class ids in .boxes.cls (moved to numpy).
|
|
273
|
+
coords= results[0].boxes.xyxy
|
|
274
|
+
labels= results[0].boxes.cls.cpu().numpy()
|
|
275
|
+
img= cv2.imread(image_path)
|
|
276
|
+
|
|
277
|
+
if name=='CNN' and mode=='Classification':
|
|
278
|
+
# Classification returns early: (predicted class index, raw scores).
|
|
279
|
+
results= results.cpu()
|
|
280
|
+
results=(results.detach().numpy())[0]
|
|
281
|
+
pred= np.argmax(results)
|
|
282
|
+
return pred, results
|
|
283
|
+
|
|
284
|
+
if key==True and len(gt_labels)>0:
|
|
285
|
+
# Draw predicted boxes with colour (255,0,0). OpenCV uses BGR,
|
|
286
|
+
# so this shows up as BLUE.
|
|
287
|
+
for i in range(0, len(coords)):
|
|
288
|
+
x0,y0= int(float(coords[i][0])),int(float(coords[i][1]))
|
|
289
|
+
x1,y1= int(float(coords[i][2])),int(float(coords[i][3]))
|
|
290
|
+
cv2.rectangle(img, (x0,y0), (x1,y1), (255,0,0), 5)
|
|
291
|
+
|
|
292
|
+
# Draw ground-truth boxes with colour (0,255,0) (GREEN).
|
|
293
|
+
# Note: COCO bbox format is [x, y, width, height], so x1/y1 here
|
|
294
|
+
# are computed as x0+width, y0+height.
|
|
295
|
+
for i in range(0, len(gt_annots)):
|
|
296
|
+
x0,y0= int(float(gt_annots[i][0])),int(float(gt_annots[i][1]))
|
|
297
|
+
x1,y1= int(float(gt_annots[i][2]))+x0,int(float(gt_annots[i][3]))+y0
|
|
298
|
+
cv2.rectangle(img, (x0,y0), (x1,y1), (0,255,0), 5)
|
|
299
|
+
|
|
300
|
+
print(labels, len(coords), len(gt_annots))
|
|
301
|
+
return len(gt_annots), coords, labels, img, confidence
|
|
302
|
+
|
|
303
|
+
def dataset_check(file):
|
|
304
|
+
"""Sanity-check a dataset folder that holds images and a COCO json.
|
|
305
|
+
|
|
306
|
+
Checks done:
|
|
307
|
+
|
|
308
|
+
1. Every image listed in the json exists in the folder.
|
|
309
|
+
2. Every annotation points to an image id that exists in the json.
|
|
310
|
+
|
|
311
|
+
A warning is printed when a check fails, and the counts are always returned.
|
|
312
|
+
|
|
313
|
+
Parameters
|
|
314
|
+
----------
|
|
315
|
+
file : str
|
|
316
|
+
Path to the dataset folder (only its top level is scanned). The first
|
|
317
|
+
``.json`` file found is used as the annotation file.
|
|
318
|
+
|
|
319
|
+
Returns
|
|
320
|
+
-------
|
|
321
|
+
n_images : int
|
|
322
|
+
Number of image files in the folder.
|
|
323
|
+
n_json_images : int
|
|
324
|
+
Number of images listed in the json.
|
|
325
|
+
n_annotated_images : int
|
|
326
|
+
Number of distinct images that have at least one annotation.
|
|
327
|
+
n_categories : int
|
|
328
|
+
Number of categories defined in the json.
|
|
329
|
+
n_used_categories : int
|
|
330
|
+
Number of distinct categories actually used by annotations.
|
|
331
|
+
|
|
332
|
+
Examples
|
|
333
|
+
--------
|
|
334
|
+
>>> counts = dataset_check("my_dataset")
|
|
335
|
+
"""
|
|
336
|
+
list_files= os.listdir(file)
|
|
337
|
+
# Locate the annotation file (first file ending with '.json').
|
|
338
|
+
json_path= [list_files[i] for i in range(0, len(list_files)) if list_files[i].endswith('.json')]
|
|
339
|
+
json_annotation_categories=[]
|
|
340
|
+
print(json_path)
|
|
341
|
+
json_path= os.path.join(file, json_path[0])
|
|
342
|
+
# Collect image files by any recognized image extension (case-insensitive)
|
|
343
|
+
# instead of only '.png'.
|
|
344
|
+
img_list= [os.path.basename(list_files[i]) for i in range(0, len(list_files)) if list_files[i].lower().endswith(IMAGE_EXTENSIONS)]
|
|
345
|
+
|
|
346
|
+
with open(json_path, 'r') as f:
|
|
347
|
+
json_file= json.load(f)
|
|
348
|
+
|
|
349
|
+
# Check that every image referenced in the COCO file is present in img_list.
|
|
350
|
+
for i in range(0,len(json_file['images'])):
|
|
351
|
+
if json_file['images'][i]['file_name'] not in img_list:
|
|
352
|
+
print('json images are not properly subset of the listed images!')
|
|
353
|
+
# return 0
|
|
354
|
+
|
|
355
|
+
json_img_list= [json_file['images'][i]['id'] for i in range(0, len(json_file['images']))]
|
|
356
|
+
|
|
357
|
+
# Check that every annotation's image_id corresponds to a known image.
|
|
358
|
+
for i in range(0, len(json_file['annotations'])):
|
|
359
|
+
if json_file['annotations'][i]['image_id'] not in json_img_list:
|
|
360
|
+
print('json annotations do not exists for few images!')
|
|
361
|
+
# return 0
|
|
362
|
+
|
|
363
|
+
json_annot_list= [json_file['annotations'][i]['image_id'] for i in range(0, len(json_file['annotations']))]
|
|
364
|
+
json_annotation_categories= [json_file['annotations'][i]['category_id'] for i in range(0, len(json_file['annotations']))]
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
return len(img_list), len(json_img_list), len(np.unique(json_annot_list)), len(json_file['categories']), len(np.unique(json_annotation_categories))
|
|
368
|
+
|
|
369
|
+
def provide_annotations(json_path, file_name, key='COCO'):
|
|
370
|
+
"""Get the ground-truth boxes and labels for one image.
|
|
371
|
+
|
|
372
|
+
Two annotation formats are supported, chosen with ``key``.
|
|
373
|
+
|
|
374
|
+
Parameters
|
|
375
|
+
----------
|
|
376
|
+
json_path : str or list of str
|
|
377
|
+
``key='COCO'``: path to one COCO-format json file.
|
|
378
|
+
``key='Vanila'``: list of json file paths (all are read and merged).
|
|
379
|
+
Each json is a list of entries with ``entry['image']['pathname']`` and
|
|
380
|
+
``entry['objects']``; each object has ``'bounding_box'`` (with
|
|
381
|
+
``'minimum'`` and ``'maximum'``, each holding ``'r'`` and ``'c'``) and
|
|
382
|
+
``'category'``.
|
|
383
|
+
file_name : str
|
|
384
|
+
Image file name, or a full path (only the last part is used).
|
|
385
|
+
key : {'COCO', 'Vanila'}, optional
|
|
386
|
+
Annotation format. Default is ``'COCO'``.
|
|
387
|
+
|
|
388
|
+
Returns
|
|
389
|
+
-------
|
|
390
|
+
``key='COCO'`` returns ``(coords, labels)``:
|
|
391
|
+
coords : list of list of float
|
|
392
|
+
Boxes in COCO format ``[x, y, width, height]``.
|
|
393
|
+
labels : list of str
|
|
394
|
+
Category names, one per box.
|
|
395
|
+
``key='Vanila'`` returns ``coords``:
|
|
396
|
+
coords : list of list
|
|
397
|
+
``[min_r, min_c, max_r, max_c, label]`` for each object
|
|
398
|
+
(``r`` = row, ``c`` = column), or ``0`` if the image is not found.
|
|
399
|
+
|
|
400
|
+
Examples
|
|
401
|
+
--------
|
|
402
|
+
>>> coords, labels = provide_annotations("annotations.json", "img_01.png")
|
|
403
|
+
>>> coords = provide_annotations(["a.json", "b.json"], "img_01.png", key="Vanila")
|
|
404
|
+
"""
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
if key=='COCO':
|
|
408
|
+
with open(json_path, 'r') as f:
|
|
409
|
+
json_file= json.load(f)
|
|
410
|
+
|
|
411
|
+
file_name= os.path.basename(file_name)
|
|
412
|
+
def get_annots(json_file, image_id):
|
|
413
|
+
"""Return all bboxes and category NAMES (looked up from
|
|
414
|
+
``json_file['categories']``) whose annotations belong to the
|
|
415
|
+
COCO image id ``image_id``."""
|
|
416
|
+
coords, labels=[],[]
|
|
417
|
+
for i in range(0, len(json_file['annotations'])):
|
|
418
|
+
annot_id= json_file['annotations'][i]['image_id']
|
|
419
|
+
if annot_id==image_id:
|
|
420
|
+
coords.append(json_file['annotations'][i]['bbox'])
|
|
421
|
+
label=json_file['annotations'][i]['category_id']
|
|
422
|
+
label=[json_file['categories'][j]['name'] for j in range(0, len(json_file['categories'])) if json_file['categories'][j]['id']==label]
|
|
423
|
+
labels.append(label[0])
|
|
424
|
+
return coords, labels
|
|
425
|
+
|
|
426
|
+
# Find the image entry matching file_name, then fetch its annotations.
|
|
427
|
+
coords, labels=[],[]
|
|
428
|
+
for i in range(0, len(json_file['images'])):
|
|
429
|
+
image_file= json_file['images'][i]['file_name']
|
|
430
|
+
|
|
431
|
+
if image_file==file_name:
|
|
432
|
+
print(f'Image {image_file} found!')
|
|
433
|
+
image_id= json_file['images'][i]['id']
|
|
434
|
+
coords, labels= get_annots(json_file, image_id)
|
|
435
|
+
|
|
436
|
+
if len(coords)==0:
|
|
437
|
+
print(f'No coordinate annotations found for image {file_name}')
|
|
438
|
+
return coords, labels
|
|
439
|
+
else:
|
|
440
|
+
print(f'Coordinate annotations found for image {file_name}')
|
|
441
|
+
return coords, labels
|
|
442
|
+
|
|
443
|
+
if key=='Vanila':
|
|
444
|
+
# Build a lookup table {image name: [[min_r, min_c, max_r, max_c, label], ...]}
|
|
445
|
+
# from all the json files in the list.
|
|
446
|
+
img_bank={}
|
|
447
|
+
for index in range(0, len(json_path)):
|
|
448
|
+
with open (json_path[index],'rb') as f:
|
|
449
|
+
json_file=json.load(f)
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
for i in range(0, len(json_file)):
|
|
453
|
+
img_name= os.path.basename(json_file[i]['image']['pathname'])
|
|
454
|
+
img_bank[img_name]=[]
|
|
455
|
+
|
|
456
|
+
coord_list= json_file[i]['objects']
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
for j in range(0, len(coord_list)):
|
|
460
|
+
coord_min_x, coord_min_y= int(coord_list[j]['bounding_box']['minimum']['r']), int(coord_list[j]['bounding_box']['minimum']['c'])
|
|
461
|
+
coord_max_x, coord_max_y= int(coord_list[j]['bounding_box']['maximum']['r']), int(coord_list[j]['bounding_box']['maximum']['c'])
|
|
462
|
+
label= coord_list[j]['category']
|
|
463
|
+
|
|
464
|
+
img_bank[img_name].append([coord_min_x, coord_min_y, coord_max_x, coord_max_y, label])
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def img_search(file_name):
|
|
468
|
+
"""Look up an image name in ``img_bank`` and return its list of
|
|
469
|
+
boxes, or ``0`` if the image is not in the bank."""
|
|
470
|
+
img_name= os.path.basename(file_name)
|
|
471
|
+
img_name_list= list(img_bank.keys())
|
|
472
|
+
coords=0
|
|
473
|
+
|
|
474
|
+
if img_name in img_name_list:
|
|
475
|
+
coords= img_bank[img_name]
|
|
476
|
+
return coords
|
|
477
|
+
|
|
478
|
+
coords=img_search(file_name)
|
|
479
|
+
return coords
|
|
480
|
+
|
|
481
|
+
def SAHI_generation(image_path, output_file_name='tiled', output_dir='SAHI_results', slice_height=256, slice_width=256, overlap_h=0, overlap_w=0):
|
|
482
|
+
"""Cut one large image into smaller tiles (SAHI slicing).
|
|
483
|
+
|
|
484
|
+
The tiles are saved to a folder, which is useful for running a detector on
|
|
485
|
+
small objects in big images. If ``output_dir`` already exists it is replaced
|
|
486
|
+
by the new tiles.
|
|
487
|
+
|
|
488
|
+
Parameters
|
|
489
|
+
----------
|
|
490
|
+
image_path : str
|
|
491
|
+
Path to the image to slice.
|
|
492
|
+
output_file_name : str, optional
|
|
493
|
+
Base name for the saved tiles. Default is ``'tiled'``.
|
|
494
|
+
output_dir : str, optional
|
|
495
|
+
Folder where the tiles are saved. Default is ``'SAHI_results'``.
|
|
496
|
+
slice_height : int, optional
|
|
497
|
+
Tile height in pixels. Default is 256.
|
|
498
|
+
slice_width : int, optional
|
|
499
|
+
Tile width in pixels. Default is 256.
|
|
500
|
+
overlap_h : float, optional
|
|
501
|
+
Vertical overlap between tiles as a fraction of the tile height
|
|
502
|
+
(0 to 1). Default is 0 (no overlap).
|
|
503
|
+
overlap_w : float, optional
|
|
504
|
+
Horizontal overlap between tiles as a fraction of the tile width
|
|
505
|
+
(0 to 1). Default is 0 (no overlap).
|
|
506
|
+
|
|
507
|
+
Returns
|
|
508
|
+
-------
|
|
509
|
+
None
|
|
510
|
+
The tiles are written to ``output_dir``. Each tile file name ends with
|
|
511
|
+
its corner coordinates (``..._x0_y0_x1_y1``), which ``SAHI_analysis``
|
|
512
|
+
uses to place detections back on the full image.
|
|
513
|
+
|
|
514
|
+
Examples
|
|
515
|
+
--------
|
|
516
|
+
>>> SAHI_generation("large_image.png", slice_height=512, slice_width=512)
|
|
517
|
+
"""
|
|
518
|
+
if os.path.exists(output_dir):
|
|
519
|
+
shutil.rmtree(output_dir)
|
|
520
|
+
slice_image_result = slice_image(
|
|
521
|
+
image=image_path,
|
|
522
|
+
output_file_name=output_file_name,
|
|
523
|
+
output_dir=output_dir,
|
|
524
|
+
slice_height=slice_height,
|
|
525
|
+
slice_width=slice_width,
|
|
526
|
+
overlap_height_ratio=overlap_h,
|
|
527
|
+
overlap_width_ratio=overlap_w,
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
def SAHI_analysis(image_path, model_det, model_clss=None, output_dir= 'SAHI_results',key=1):
|
|
531
|
+
"""Run detection (and optional classification) on SAHI tiles and draw
|
|
532
|
+
the results on the full image.
|
|
533
|
+
|
|
534
|
+
For every tile in ``output_dir``: detect objects, optionally classify each
|
|
535
|
+
detected crop, shift the box from tile coordinates to full-image coordinates
|
|
536
|
+
and draw it on the original image.
|
|
537
|
+
|
|
538
|
+
Parameters
|
|
539
|
+
----------
|
|
540
|
+
image_path : str
|
|
541
|
+
Path to the original full-size image (the one that was tiled).
|
|
542
|
+
model_det : object
|
|
543
|
+
Detection model (needs ``.predict()``), used through ``ai_analytics``.
|
|
544
|
+
model_clss : object, optional
|
|
545
|
+
Classification model (CNN) applied to every detected crop. If ``None``
|
|
546
|
+
(default), no classification is done.
|
|
547
|
+
output_dir : str, optional
|
|
548
|
+
Folder containing the tiles made by ``SAHI_generation``. Default is
|
|
549
|
+
``'SAHI_results'``.
|
|
550
|
+
key : {0, 1}, optional
|
|
551
|
+
Transform key passed to ``Image_Utils.cell_transform`` (0 = with ImageNet
|
|
552
|
+
normalisation, 1 = without). Default is 1.
|
|
553
|
+
|
|
554
|
+
Returns
|
|
555
|
+
-------
|
|
556
|
+
img : numpy.ndarray
|
|
557
|
+
The full image (BGR) with the boxes drawn. Without a classifier every
|
|
558
|
+
box is blue. With a classifier, predicted class 0 is drawn green and any
|
|
559
|
+
other class red.
|
|
560
|
+
|
|
561
|
+
Examples
|
|
562
|
+
--------
|
|
563
|
+
>>> SAHI_generation("large_image.png")
|
|
564
|
+
>>> result = SAHI_analysis("large_image.png", det_model, cnn_model)
|
|
565
|
+
"""
|
|
566
|
+
image_slice_path, json_path, txt_path= img_loc(output_dir)
|
|
567
|
+
IMAGE_EXTENSIONS = ('png', 'jpg', 'jpeg', 'bmp', 'tif', 'tiff', 'webp')
|
|
568
|
+
img=cv2.imread(image_path)
|
|
569
|
+
|
|
570
|
+
for i in range(0, len(image_slice_path)):
|
|
571
|
+
img_name = os.path.splitext(image_slice_path[i])[0]
|
|
572
|
+
x_n0,y_n0= int(img_name.split('_')[-4]), int(img_name.split('_')[-3])
|
|
573
|
+
x_n1, y_n1= int(img_name.split('_')[-2]), int(img_name.split('_')[-1])
|
|
574
|
+
print(img_name, x_n0, y_n0, x_n1, y_n1)
|
|
575
|
+
len_gt, coords,_,_,_= ai_analytics(None, image_slice_path[i], model_det)
|
|
576
|
+
img_SAHI= cv2.imread(image_slice_path[i])
|
|
577
|
+
for j in range(0, len(coords)):
|
|
578
|
+
x0,y0= int(float(coords[j][0])),int(float(coords[j][1]))
|
|
579
|
+
x1,y1= int(float(coords[j][2])),int(float(coords[j][3]))
|
|
580
|
+
color= (255,0,0)
|
|
581
|
+
if model_clss:
|
|
582
|
+
img_slice= img_SAHI[y0:y1, x0:x1]
|
|
583
|
+
img_slice= cell_geometry(img_slice)
|
|
584
|
+
img_slice= cv2.resize(img_slice, (224,224))
|
|
585
|
+
img_slice= cv2.cvtColor(img_slice, cv2.COLOR_BGR2RGB)
|
|
586
|
+
img_slice= cell_transform(img_slice, key)[np.newaxis,:,:]
|
|
587
|
+
pred, results= ai_analytics(None, img_slice, model_clss, name='CNN',mode='Classification')
|
|
588
|
+
print('pred', pred)
|
|
589
|
+
if pred==0:
|
|
590
|
+
color= (0,255,0)
|
|
591
|
+
else:
|
|
592
|
+
color=(0,0,255)
|
|
593
|
+
x0,y0= x0+x_n0, y0+y_n0
|
|
594
|
+
x1,y1= x1+x_n0, y1+y_n0
|
|
595
|
+
cv2.rectangle(img, (x0,y0), (x1,y1), color,2)
|
|
596
|
+
|
|
597
|
+
return img
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"""Image pre-processing helpers.
|
|
2
|
+
|
|
3
|
+
Functions that prepare images (for example single-cell crops) before they are
|
|
4
|
+
shown to a classifier or used for training: normalisation, augmentation,
|
|
5
|
+
padding to a square and conversion to tensors. Images are NumPy arrays read
|
|
6
|
+
with OpenCV, so colour images are in BGR order unless stated otherwise.
|
|
7
|
+
|
|
8
|
+
Functions
|
|
9
|
+
---------
|
|
10
|
+
image_processing
|
|
11
|
+
Colour / contrast normalisation (CLAHE, Reinhard colour transfer or
|
|
12
|
+
Otsu thresholding).
|
|
13
|
+
data_augmentation
|
|
14
|
+
Generates rotated and blurred copies of an image.
|
|
15
|
+
cell_geometry
|
|
16
|
+
Makes an image even-sized and pads it with black to a square.
|
|
17
|
+
cell_transform
|
|
18
|
+
Converts an image to a PyTorch tensor, with or without ImageNet
|
|
19
|
+
normalisation.
|
|
20
|
+
|
|
21
|
+
Author: Shounak Sanyal
|
|
22
|
+
"""
|
|
23
|
+
import os
|
|
24
|
+
import cv2
|
|
25
|
+
import matplotlib.pyplot as plt
|
|
26
|
+
import numpy as np
|
|
27
|
+
from torchvision.transforms import v2
|
|
28
|
+
import os, cv2, json, torch, shutil
|
|
29
|
+
from torchvision import datasets, models, transforms
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def image_processing(img, ref_img=None, normalisation='Clahe', grid_window=(8,8)):
|
|
33
|
+
"""Normalise an image using CLAHE, Reinhard colour transfer or Otsu.
|
|
34
|
+
|
|
35
|
+
Parameters
|
|
36
|
+
----------
|
|
37
|
+
img : numpy.ndarray
|
|
38
|
+
Input image. BGR for ``'Clahe'`` (grayscale also works) and
|
|
39
|
+
``'Reinhard'``; single-channel 8-bit grayscale for ``'Otsu'``.
|
|
40
|
+
ref_img : numpy.ndarray, optional
|
|
41
|
+
Reference image in BGR format. Needed only for ``'Reinhard'``.
|
|
42
|
+
normalisation : {'Clahe', 'Reinhard', 'Otsu'}, optional
|
|
43
|
+
Method to use (case-sensitive). Default is ``'Clahe'``.
|
|
44
|
+
|
|
45
|
+
* ``'Clahe'``: improves local contrast by equalising the lightness
|
|
46
|
+
channel (adaptive histogram equalisation, clip limit 2.0).
|
|
47
|
+
* ``'Reinhard'``: matches the colour statistics of ``img`` to those of
|
|
48
|
+
``ref_img``.
|
|
49
|
+
* ``'Otsu'``: automatic thresholding that returns an inverted binary
|
|
50
|
+
mask (values 0 and 255).
|
|
51
|
+
grid_window : tuple of int, optional
|
|
52
|
+
CLAHE tile grid size ``(rows, cols)``. Used only by ``'Clahe'``.
|
|
53
|
+
Default is ``(8, 8)``.
|
|
54
|
+
|
|
55
|
+
Returns
|
|
56
|
+
-------
|
|
57
|
+
img_norm : numpy.ndarray
|
|
58
|
+
The normalised image (BGR for colour input, single-channel for
|
|
59
|
+
grayscale input and ``'Otsu'``).
|
|
60
|
+
|
|
61
|
+
Examples
|
|
62
|
+
--------
|
|
63
|
+
>>> img_norm = image_processing(img) # CLAHE
|
|
64
|
+
>>> img_norm = image_processing(img, ref_img, "Reinhard") # match colours
|
|
65
|
+
"""
|
|
66
|
+
# --- Reinhard Color Transfer ---
|
|
67
|
+
if normalisation == 'Reinhard':
|
|
68
|
+
# Convert images from BGR to LAB color space (float32 for precise math)
|
|
69
|
+
img_lab = cv2.cvtColor(img, cv2.COLOR_BGR2LAB).astype(np.float32)
|
|
70
|
+
ref_lab = cv2.cvtColor(ref_img, cv2.COLOR_BGR2LAB).astype(np.float32)
|
|
71
|
+
|
|
72
|
+
# Calculate mean and standard deviation for reference image
|
|
73
|
+
ref_mean = ref_lab.reshape(-1, 3).mean(axis=0)
|
|
74
|
+
ref_sd = ref_lab.reshape(-1, 3).std(axis=0)
|
|
75
|
+
|
|
76
|
+
# Calculate mean and standard deviation for input image
|
|
77
|
+
img_mean = img_lab.reshape(-1, 3).mean(axis=0)
|
|
78
|
+
img_sd = img_lab.reshape(-1, 3).std(axis=0)
|
|
79
|
+
|
|
80
|
+
# Normalize channel distributions to match reference statistics
|
|
81
|
+
normalized = (
|
|
82
|
+
(img_lab - img_mean) / (img_sd + 1e-8) * ref_sd + ref_mean
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Ensure pixel values stay within valid range [0, 255]
|
|
86
|
+
normalized = np.clip(normalized, 0, 255).astype(np.uint8)
|
|
87
|
+
img_norm = cv2.cvtColor(normalized, cv2.COLOR_Lab2BGR)
|
|
88
|
+
|
|
89
|
+
# --- CLAHE (Contrast Limited Adaptive Histogram Equalization) ---
|
|
90
|
+
if normalisation == 'Clahe':
|
|
91
|
+
# Convert BGR image to LAB color space
|
|
92
|
+
try:
|
|
93
|
+
lab = cv2.cvtColor(img, cv2.COLOR_BGR2LAB)
|
|
94
|
+
|
|
95
|
+
# Configure CLAHE parameters
|
|
96
|
+
clahe = cv2.createCLAHE(
|
|
97
|
+
clipLimit=2.0,
|
|
98
|
+
tileGridSize=grid_window
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Apply histogram equalization exclusively to the L (lightness) channel
|
|
102
|
+
lab[:, :, 0] = clahe.apply(lab[:, :, 0])
|
|
103
|
+
|
|
104
|
+
# Convert image back from LAB to BGR color space
|
|
105
|
+
img_norm = cv2.cvtColor(lab, cv2.COLOR_LAB2BGR)
|
|
106
|
+
except:
|
|
107
|
+
clahe = cv2.createCLAHE(
|
|
108
|
+
clipLimit=2.0,
|
|
109
|
+
tileGridSize=grid_window
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
img_norm= clahe.apply(img)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
if normalisation=='Otsu':
|
|
116
|
+
_, img_norm= cv2.threshold(img, 0,255, cv2.THRESH_BINARY_INV+cv2.THRESH_OTSU)
|
|
117
|
+
return img_norm
|
|
118
|
+
|
|
119
|
+
def data_augmentation(img, n_rotate=3, blur=True):
|
|
120
|
+
"""Generate rotated (and optionally blurred) copies of an image.
|
|
121
|
+
|
|
122
|
+
Parameters
|
|
123
|
+
----------
|
|
124
|
+
img : numpy.ndarray
|
|
125
|
+
Input image.
|
|
126
|
+
n_rotate : int, optional
|
|
127
|
+
Number of rotations to apply, from 0 to 3. Default is 3. The rotations
|
|
128
|
+
are, in order: 90 degrees clockwise, 180 degrees, 90 degrees
|
|
129
|
+
counter-clockwise.
|
|
130
|
+
blur : bool, optional
|
|
131
|
+
If True, also add a Gaussian-blurred copy of the original image
|
|
132
|
+
(5x5 kernel, sigma 1). Default is True.
|
|
133
|
+
|
|
134
|
+
Returns
|
|
135
|
+
-------
|
|
136
|
+
img_aug : list of numpy.ndarray
|
|
137
|
+
The rotated images in order, followed by the blurred image when
|
|
138
|
+
``blur`` is True. The original image is not included.
|
|
139
|
+
|
|
140
|
+
Examples
|
|
141
|
+
--------
|
|
142
|
+
>>> augmented = data_augmentation(img) # 3 rotations + 1 blur = 4 images
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
img_aug=[]
|
|
146
|
+
|
|
147
|
+
for i in range(0,n_rotate):
|
|
148
|
+
|
|
149
|
+
img_aug.append(cv2.rotate(img, i))
|
|
150
|
+
|
|
151
|
+
if blur:
|
|
152
|
+
|
|
153
|
+
img_aug.append(cv2.GaussianBlur(img, (5,5),1))
|
|
154
|
+
|
|
155
|
+
return img_aug
|
|
156
|
+
|
|
157
|
+
def cell_geometry(img):
|
|
158
|
+
"""Make an image even-sized and pad it with black to a square.
|
|
159
|
+
|
|
160
|
+
Steps:
|
|
161
|
+
|
|
162
|
+
1. If the height or width is odd, that side is resized by +1 pixel.
|
|
163
|
+
2. If the image is not square, the shorter side is padded with black so the
|
|
164
|
+
original image stays centred. The result is square, with side equal to
|
|
165
|
+
the longer dimension.
|
|
166
|
+
|
|
167
|
+
Parameters
|
|
168
|
+
----------
|
|
169
|
+
img : numpy.ndarray
|
|
170
|
+
Grayscale image of shape ``(H, W)`` or colour image of shape
|
|
171
|
+
``(H, W, C)``.
|
|
172
|
+
|
|
173
|
+
Returns
|
|
174
|
+
-------
|
|
175
|
+
numpy.ndarray
|
|
176
|
+
Square image of dtype ``uint8``.
|
|
177
|
+
|
|
178
|
+
Examples
|
|
179
|
+
--------
|
|
180
|
+
>>> square = cell_geometry(crop) # e.g. a 40x60 crop becomes 60x60
|
|
181
|
+
"""
|
|
182
|
+
# Get dimensions
|
|
183
|
+
if img.ndim == 3:
|
|
184
|
+
h, w, c = img.shape
|
|
185
|
+
elif img.ndim == 2:
|
|
186
|
+
h, w = img.shape
|
|
187
|
+
else:
|
|
188
|
+
raise ValueError(f"Expected 2D or 3D image, got shape {img.shape}")
|
|
189
|
+
|
|
190
|
+
# Make dimensions even
|
|
191
|
+
if h % 2 != 0:
|
|
192
|
+
h_new = h + 1
|
|
193
|
+
img = cv2.resize(img, (w, h_new))
|
|
194
|
+
h = h_new
|
|
195
|
+
|
|
196
|
+
if w % 2 != 0:
|
|
197
|
+
w_new = w + 1
|
|
198
|
+
img = cv2.resize(img, (w_new, h))
|
|
199
|
+
w = w_new
|
|
200
|
+
|
|
201
|
+
# Already square
|
|
202
|
+
if h == w:
|
|
203
|
+
return img.astype(np.uint8)
|
|
204
|
+
|
|
205
|
+
# Width > height
|
|
206
|
+
if w > h:
|
|
207
|
+
dx = w - h
|
|
208
|
+
|
|
209
|
+
if img.ndim == 3:
|
|
210
|
+
new_img = np.zeros((w, w, c), dtype=np.uint8)
|
|
211
|
+
else:
|
|
212
|
+
new_img = np.zeros((w, w), dtype=np.uint8)
|
|
213
|
+
|
|
214
|
+
top = dx // 2
|
|
215
|
+
bottom = top + h
|
|
216
|
+
|
|
217
|
+
new_img[top:bottom, :] = img
|
|
218
|
+
|
|
219
|
+
# Height > width
|
|
220
|
+
else:
|
|
221
|
+
dy = h - w
|
|
222
|
+
|
|
223
|
+
if img.ndim == 3:
|
|
224
|
+
new_img = np.zeros((h, h, c), dtype=np.uint8)
|
|
225
|
+
else:
|
|
226
|
+
new_img = np.zeros((h, h), dtype=np.uint8)
|
|
227
|
+
|
|
228
|
+
left = dy // 2
|
|
229
|
+
right = left + w
|
|
230
|
+
|
|
231
|
+
new_img[:, left:right] = img
|
|
232
|
+
|
|
233
|
+
return new_img
|
|
234
|
+
|
|
235
|
+
def cell_transform(img, key):
|
|
236
|
+
"""Convert an image to a float PyTorch tensor for a classifier.
|
|
237
|
+
|
|
238
|
+
Parameters
|
|
239
|
+
----------
|
|
240
|
+
img : numpy.ndarray or PIL.Image.Image
|
|
241
|
+
Input image, typically RGB ``uint8`` of shape ``(H, W, 3)``.
|
|
242
|
+
key : {0, 1}
|
|
243
|
+
Transform to apply.
|
|
244
|
+
|
|
245
|
+
* ``0``: convert to a tensor with values in ``[0, 1]`` and normalise
|
|
246
|
+
with the ImageNet mean ``[0.485, 0.456, 0.406]`` and standard
|
|
247
|
+
deviation ``[0.229, 0.224, 0.225]``.
|
|
248
|
+
* ``1``: convert to a tensor with values in ``[0, 1]`` only (no
|
|
249
|
+
normalisation).
|
|
250
|
+
|
|
251
|
+
Returns
|
|
252
|
+
-------
|
|
253
|
+
img : torch.Tensor
|
|
254
|
+
Tensor of shape ``(C, H, W)`` and dtype ``float32``.
|
|
255
|
+
|
|
256
|
+
Examples
|
|
257
|
+
--------
|
|
258
|
+
>>> tensor = cell_transform(rgb_img, 0)
|
|
259
|
+
"""
|
|
260
|
+
if key==0:
|
|
261
|
+
transform=v2.Compose([v2.ToImage(), v2.ToDtype(torch.float32, scale=True), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])])
|
|
262
|
+
if key==1:
|
|
263
|
+
transform=v2.Compose([v2.ToImage(), v2.ToDtype(torch.float32, scale=True)])
|
|
264
|
+
img= transform(img)
|
|
265
|
+
return img
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Computer_vision_assisting_tools
|
|
2
|
+
================================
|
|
3
|
+
|
|
4
|
+
A small toolbox of helpers for computer-vision projects: image
|
|
5
|
+
pre-processing, dataset / annotation handling, model inference, SAHI tiling
|
|
6
|
+
and detection metrics.
|
|
7
|
+
|
|
8
|
+
Modules
|
|
9
|
+
-------
|
|
10
|
+
Image_Utils
|
|
11
|
+
Image normalisation, augmentation, padding and tensor transforms.
|
|
12
|
+
Basic_Utils
|
|
13
|
+
File discovery, COCO dataset checks, annotation lookup, model inference
|
|
14
|
+
and SAHI tiling.
|
|
15
|
+
AI_Metrics
|
|
16
|
+
IoU, precision / recall and mAP for object detection.
|
|
17
|
+
|
|
18
|
+
See ``README.md`` for the full description and the module hierarchy.
|
|
19
|
+
|
|
20
|
+
Author: Shounak Sanyal
|
|
21
|
+
"""
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: Computer_Vision_Assistant_Package
|
|
3
|
+
Version: 1.0
|
|
4
|
+
Summary: A user friendly package assiting in the most common computer vision tasks
|
|
5
|
+
Author-email: Shounak Sanyal <shounaksanyal1902@outlook.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: License.txt
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Requires-Python: >=3.12
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# Computer_vision_assisting_tools
|
|
13
|
+
|
|
14
|
+
**Author:** Shounak Sanyal
|
|
15
|
+
|
|
16
|
+
A small toolbox of helpers for computer-vision projects. It bundles the
|
|
17
|
+
functions that were most useful in a detection + classification workflow:
|
|
18
|
+
|
|
19
|
+
* find images and annotation files in a folder tree,
|
|
20
|
+
* check and read COCO-style datasets,
|
|
21
|
+
* run a detector (RF-DETR, YOLO) or a CNN classifier on an image and draw the
|
|
22
|
+
predictions next to the ground truth,
|
|
23
|
+
* slice large images into tiles (SAHI) and run detection / classification on
|
|
24
|
+
every tile,
|
|
25
|
+
* pre-process images (CLAHE, Reinhard colour transfer, Otsu, augmentation,
|
|
26
|
+
square padding, tensor transforms),
|
|
27
|
+
* score a detector with IoU, precision / recall and mAP.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Package hierarchy
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
.
|
|
35
|
+
├── README.md
|
|
36
|
+
└── Computer_vision_assisting_tools/
|
|
37
|
+
├── __innit__.py Package docstring (no code)
|
|
38
|
+
├── AI_Metrics.py Detection metrics
|
|
39
|
+
│ ├── IOU_calc
|
|
40
|
+
│ ├── mAP_calc
|
|
41
|
+
│ ├── AUC_calc
|
|
42
|
+
│ └── detection_metrics
|
|
43
|
+
├── Basic_Utils.py Files, datasets, annotations, inference, SAHI
|
|
44
|
+
│ ├── IMAGE_EXTENSIONS (constant)
|
|
45
|
+
│ ├── img_loc
|
|
46
|
+
│ ├── ai_analytics
|
|
47
|
+
│ ├── dataset_check
|
|
48
|
+
│ ├── provide_annotations
|
|
49
|
+
│ ├── SAHI_generation
|
|
50
|
+
│ └── SAHI_analysis
|
|
51
|
+
└── Image_Utils.py Image pre-processing
|
|
52
|
+
├── image_processing
|
|
53
|
+
├── data_augmentation
|
|
54
|
+
├── cell_geometry
|
|
55
|
+
└── cell_transform
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### How the modules depend on each other
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
AI_Metrics.py ──► Basic_Utils.py ──► Image_Utils.py
|
|
62
|
+
(metrics) (data + models) (image helpers)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`AI_Metrics` uses `provide_annotations` and `ai_analytics` from `Basic_Utils`;
|
|
66
|
+
`Basic_Utils` uses `cell_geometry` and `cell_transform` from `Image_Utils`.
|
|
67
|
+
`Image_Utils` depends on none of the others. The modules import each other
|
|
68
|
+
with relative imports (`from .Basic_Utils import *`), so they must be used as
|
|
69
|
+
part of the package, not run directly as scripts.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Module descriptions
|
|
74
|
+
|
|
75
|
+
### `Image_Utils.py` — image pre-processing
|
|
76
|
+
|
|
77
|
+
Helpers that prepare images (for example single-cell crops) before they reach
|
|
78
|
+
a classifier. Images are NumPy arrays read with OpenCV, so colour images are in
|
|
79
|
+
BGR order unless stated otherwise.
|
|
80
|
+
|
|
81
|
+
| Function | What it does |
|
|
82
|
+
|---|---|
|
|
83
|
+
| `image_processing(img, ref_img=None, normalisation='Clahe', grid_window=(8,8))` | Normalises an image with CLAHE (default), Reinhard colour transfer (needs `ref_img`) or Otsu thresholding (`'Otsu'`). |
|
|
84
|
+
| `data_augmentation(img, n_rotate=3, blur=True)` | Returns up to three rotated copies (90°, 180°, 270°; default 3) and, optionally, a Gaussian-blurred copy. |
|
|
85
|
+
| `cell_geometry(img)` | Makes both sides even, then pads the shorter side with black so the image becomes a centred square. |
|
|
86
|
+
| `cell_transform(img, key)` | Converts an image to a `float32` tensor. `key=0` adds ImageNet normalisation, `key=1` only scales to [0, 1]. |
|
|
87
|
+
|
|
88
|
+
### `Basic_Utils.py` — data, annotations, inference and SAHI
|
|
89
|
+
|
|
90
|
+
The main working module. It finds files, checks datasets, reads ground truth,
|
|
91
|
+
runs models and handles SAHI tiling.
|
|
92
|
+
|
|
93
|
+
| Function | What it does |
|
|
94
|
+
|---|---|
|
|
95
|
+
| `IMAGE_EXTENSIONS` | Tuple of file extensions treated as images (case-insensitive). |
|
|
96
|
+
| `img_loc(path)` | Recursively lists image, `.json` and `.txt` files under a folder. Returns `(img_list, json_list, txt_list)`. |
|
|
97
|
+
| `ai_analytics(file, image_path, model, name='RFDETR', key=True, mode='Detection')` | Runs RF-DETR / YOLO detection or CNN classification on one image (the model is moved to the GPU if available). For detection it also reads the COCO ground truth and can draw predictions (blue) and ground truth (green). |
|
|
98
|
+
| `dataset_check(file)` | Checks that the images in a folder and the COCO json agree. Returns five counts (images, json images, annotated images, categories, used categories). |
|
|
99
|
+
| `provide_annotations(json_path, file_name, key='COCO')` | Returns the ground-truth boxes and labels for one image, from a COCO json or from the `'Vanila'` multi-json format. |
|
|
100
|
+
| `SAHI_generation(image_path, ...)` | Slices a large image into tiles and saves them (the output folder is deleted first if it exists). |
|
|
101
|
+
| `SAHI_analysis(image_path, model_det, model_clss=None, ...)` | Detects (and optionally classifies) objects on every tile and draws them on the full image. |
|
|
102
|
+
|
|
103
|
+
### `AI_Metrics.py` — detection metrics
|
|
104
|
+
|
|
105
|
+
Scores a detector on a single image against COCO ground truth. Boxes are in
|
|
106
|
+
`(x_min, y_min, x_max, y_max)` format.
|
|
107
|
+
|
|
108
|
+
| Function | What it does |
|
|
109
|
+
|---|---|
|
|
110
|
+
| `IOU_calc(target_pts, gt_pts)` | IoU of one box against a list of ground-truth boxes. |
|
|
111
|
+
| `mAP_calc(bboxes, gt_bboxes)` | Running precision and recall for IoU thresholds 0.50 – 0.90. |
|
|
112
|
+
| `AUC_calc(precision, recall)` | Builds the smoothed precision curve used for the area under the curve. |
|
|
113
|
+
| `detection_metrics(json_file, img_file, model_test)` | End-to-end evaluation. Returns `(mAP_50, mAP_50_95)`. |
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Requirements
|
|
118
|
+
|
|
119
|
+
* Python 3.9+
|
|
120
|
+
* `numpy`
|
|
121
|
+
* `opencv-python`
|
|
122
|
+
* `torch`
|
|
123
|
+
* `torchvision` (0.16 or newer, for `torchvision.transforms.v2.ToImage`)
|
|
124
|
+
* `matplotlib` (imported by the modules)
|
|
125
|
+
* `sahi`
|
|
126
|
+
* A detector with a `.predict()` method (RF-DETR or Ultralytics YOLO) if you
|
|
127
|
+
use `ai_analytics`, `SAHI_analysis` or `detection_metrics`.
|
|
128
|
+
|
|
129
|
+
## Quick example
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
from Computer_vision_assisting_tools.Basic_Utils import img_loc, dataset_check
|
|
133
|
+
from Computer_vision_assisting_tools.Image_Utils import image_processing
|
|
134
|
+
|
|
135
|
+
images, jsons, txts = img_loc("my_dataset") # find all files
|
|
136
|
+
print(dataset_check("my_dataset")) # sanity-check images vs json
|
|
137
|
+
|
|
138
|
+
import cv2
|
|
139
|
+
img = cv2.imread(images[0])
|
|
140
|
+
img_clahe = image_processing(img, normalisation="Clahe")
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Documentation style
|
|
144
|
+
|
|
145
|
+
Every module, function and inner helper has a NumPy-style docstring
|
|
146
|
+
(`Parameters`, `Returns`, `Examples`). Use `help(function_name)` to read
|
|
147
|
+
them.
|
|
148
|
+
|
|
149
|
+
## Author
|
|
150
|
+
|
|
151
|
+
Created and maintained by **Shounak Sanyal**.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Computer_Vision_Assistant_Package/AI_Metrics.py,sha256=iQ3da7iqlgKuqyJlDDelegWCIhBJ5eXDJl24Z2-2o-4,8289
|
|
2
|
+
Computer_Vision_Assistant_Package/Basic_Utils.py,sha256=RPHRfnx8BYMYhPet-crzLqzngHt3KeGU9uyEUc2pLDQ,23720
|
|
3
|
+
Computer_Vision_Assistant_Package/Image_Utils.py,sha256=gGIOt-R9Odrw-B3Pw2mhy_i6nal5tFSPN9Qe51AktcE,8325
|
|
4
|
+
Computer_Vision_Assistant_Package/__innit__.py,sha256=Ee5avxb60YFZgm7DSw2qrkxq4BgpSZjg0oF8y57XUog,606
|
|
5
|
+
computer_vision_assistant_package-1.0.dist-info/METADATA,sha256=d9Een0WErNCDVCdngXyThP-esoiazh4TYNHYzEOs_fg,6420
|
|
6
|
+
computer_vision_assistant_package-1.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
7
|
+
computer_vision_assistant_package-1.0.dist-info/licenses/License.txt,sha256=PlCLU6XTp8wrSeEQOxQBUoeLxWIfc4wP71Sc6FOE_t4,1070
|
|
8
|
+
computer_vision_assistant_package-1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Shounak Sanyal
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE
|