Sensei13k commited on
Commit
bc7fea0
·
verified ·
1 Parent(s): 23fc0e0

Upload 18 files

Browse files
.dockerignore ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ exe
2
+
Dockerfile ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10
2
+
3
+ WORKDIR /app
4
+
5
+ # Install required dependencies
6
+ RUN apt-get update && apt-get install -y libgl1-mesa-glx
7
+
8
+ # Copy and install Python dependencies
9
+ COPY requirements.txt .
10
+ RUN pip install --no-cache-dir -r requirements.txt
11
+
12
+ # Copy the rest of the application code
13
+ COPY . .
14
+
15
+ # Expose port
16
+ EXPOSE 8000
17
+
18
+ # Run the application (Fix: Bind to 0.0.0.0)
19
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]
Models/New_Apparatus_model.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:71e4435577d00ab03c2e7235d725fafec791701e74e474bbddb981f5c1c9c01a
3
+ size 6585932
Models/Remaining_tests_model.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:bd46c28050ed420df8e3b6f08fbea4a54f949b1f23bf8bea0e063503f04cbdbc
3
+ size 6650636
Models/analog_box_v1.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:160cabca761c1b6f0e9b3721a0b793e2ec4752fde7b088b53fa0bbc9ff761814
3
+ size 6691340
Models/analog_box_v2.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8da74f63d7648dbbff9258faab09fb2e85e6f823f02e5db2e44bf509cf6a0f8d
3
+ size 6437324
Models/analog_reading.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:acd3a3f754cf4555b09330af12727ab1b958fea71aa66dc2a6d0bb8dc41a8393
3
+ size 6752652
Models/analog_reading_v1.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:acd3a3f754cf4555b09330af12727ab1b958fea71aa66dc2a6d0bb8dc41a8393
3
+ size 6752652
Models/analog_reading_v2.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:cc51c222ffa476ad1dadfa56937238c08149102aa49b13d3de533ba62c875c9e
3
+ size 6760652
Models/res_temp_box.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:21b3300ece908bba2b786195f36c1842a3b1583d8a5be5e0548e946c98e391ab
3
+ size 6649420
Models/res_temp_ocr.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:41b7fc8aca0bb09f51a2dcbec9e27a5cc454cbbc83a9f3e72c61e3b00d85e1ba
3
+ size 6828236
Remaining_test.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import cv2
3
+ import numpy as np
4
+ import easyocr
5
+ from ultralytics import YOLO
6
+
7
+ # Initialize EasyOCR reader
8
+ reader = easyocr.Reader(['en'])
9
+
10
+ def draw_obb(image, obb):
11
+ boxes = obb.xyxyxyxy.cpu().numpy()
12
+ extracted_texts = []
13
+
14
+ for i, box in enumerate(boxes):
15
+ pts = box.reshape(4, 2).astype(np.int32)
16
+
17
+ # Draw the bounding box
18
+ cv2.polylines(image, [pts], isClosed=True, color=(0, 255, 0), thickness=2)
19
+
20
+ # Crop the detected region
21
+ x_min, y_min = np.min(pts, axis=0)
22
+ x_max, y_max = np.max(pts, axis=0)
23
+ cropped_region = image[y_min:y_max, x_min:x_max]
24
+
25
+ # Apply OCR on the cropped region
26
+ if cropped_region.size > 0:
27
+ text_results = reader.readtext(cropped_region)
28
+ detected_text = " ".join([text[1] for text in text_results])
29
+ extracted_texts.append(detected_text)
30
+
31
+ # Put extracted text on the image
32
+ cv2.putText(image, detected_text, (x_min, y_min - 10),
33
+ cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 255), 2)
34
+
35
+ return image, extracted_texts
36
+
37
+ def main(model_path_3, image_path):
38
+ # Load the YOLO OBB model for detection
39
+ model_3 = YOLO(model_path_3)
40
+
41
+ # Read the input image
42
+ image = cv2.imread(image_path)
43
+ if image is None:
44
+ print("Error: Could not read image at", image_path)
45
+ sys.exit(1)
46
+
47
+ # Run inference using model_3 for detection
48
+ results = model_3(image)
49
+
50
+ all_extracted_texts = []
51
+
52
+ # Iterate over the results and draw OBB predictions
53
+ for r in results:
54
+ if r.obb is not None:
55
+ image, extracted_texts = draw_obb(image, r.obb)
56
+ all_extracted_texts.extend(extracted_texts)
57
+ for i, class_id in enumerate(r.obb.cls.cpu().numpy()):
58
+ class_name = r.names[int(class_id)]
59
+ print(f"Detected class ID: {class_id}, Class name: {class_name}")
60
+
61
+ # Print extracted texts from OCR
62
+ for idx, text in enumerate(extracted_texts):
63
+ print(f"OCR Extracted Text {idx + 1}: {text}")
64
+
65
+ return image, all_extracted_texts
66
+
analog.py ADDED
@@ -0,0 +1,254 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import cv2
3
+ import numpy as np
4
+ from ultralytics import YOLO
5
+
6
+ # -----------------------------
7
+ # Part 1: Helper functions for cropping
8
+ # -----------------------------
9
+
10
+ def draw_obb(image, obb):
11
+ """Draw oriented bounding boxes on an image."""
12
+ boxes = obb.xyxyxyxy.cpu().numpy()
13
+ for box in boxes:
14
+ pts = box.reshape(4, 2).astype(np.int32)
15
+ cv2.polylines(image, [pts], isClosed=True, color=(0, 255, 0), thickness=2)
16
+ return image
17
+
18
+ def order_points(pts):
19
+ """Order 4 points as top-left, top-right, bottom-right, bottom-left."""
20
+ rect = np.zeros((4, 2), dtype="float32")
21
+ s = pts.sum(axis=1)
22
+ rect[0] = pts[np.argmin(s)]
23
+ rect[2] = pts[np.argmax(s)]
24
+ diff = np.diff(pts, axis=1)
25
+ rect[1] = pts[np.argmin(diff)]
26
+ rect[3] = pts[np.argmax(diff)]
27
+ return rect
28
+
29
+ def crop_region(image, obb):
30
+ """
31
+ Crop the meter region from the image using the OBB.
32
+ Uses a perspective transformation based on the minimal area rectangle.
33
+ """
34
+ boxes = obb.xyxyxyxy.cpu().numpy()
35
+ if len(boxes) == 0:
36
+ return None
37
+ # Use the first detected box for cropping.
38
+ box = boxes[0]
39
+ pts = box.reshape(4, 2).astype(np.float32)
40
+
41
+ # Get the minimal area rectangle for the points.
42
+ rect = cv2.minAreaRect(pts)
43
+ width = int(rect[1][0])
44
+ height = int(rect[1][1])
45
+ if width <= 0 or height <= 0:
46
+ return None
47
+
48
+ # Destination points for the warp (top-left, top-right, bottom-right, bottom-left)
49
+ dst_pts = np.array([
50
+ [0, 0],
51
+ [width - 1, 0],
52
+ [width - 1, height - 1],
53
+ [0, height - 1]], dtype=np.float32)
54
+
55
+ # Order the source points and compute the perspective transform.
56
+ ordered_pts = order_points(pts)
57
+ M = cv2.getPerspectiveTransform(ordered_pts, dst_pts)
58
+ cropped = cv2.warpPerspective(image, M, (width, height))
59
+ return cropped
60
+
61
+ def detect_and_crop_region(analog_box_model, image_path):
62
+ """
63
+ Detect the meter region using analog_box.pt and return the cropped image.
64
+ """
65
+ model = YOLO(analog_box_model)
66
+ image = cv2.imread(image_path)
67
+ if image is None:
68
+ print("Error: Could not read image at", image_path)
69
+ sys.exit(1)
70
+
71
+ results = model(image)
72
+ for r in results:
73
+ if hasattr(r, "obb") and r.obb is not None:
74
+ cropped = crop_region(image, r.obb)
75
+ if cropped is not None:
76
+ return cropped
77
+ print("No meter detected.")
78
+ sys.exit(1)
79
+
80
+ # -----------------------------
81
+ # Part 2: Meter reading functions (provided calculation code)
82
+ # -----------------------------
83
+
84
+ def get_center_point(box):
85
+ """Calculate the center point of a bounding box (4 corners)."""
86
+ pts = box.reshape(4, 2)
87
+ center_x = np.mean(pts[:, 0])
88
+ center_y = np.mean(pts[:, 1])
89
+ return (center_x, center_y)
90
+
91
+ def calculate_meter_reading(needle_corners, number_positions):
92
+ """
93
+ Given the needle corners and number positions, calculate the meter reading.
94
+ The numbers are standardized as [0, 5, 10, 15, 20, 25, 30].
95
+ """
96
+ number_values = [0, 5, 10, 15, 20, 25, 30]
97
+
98
+ # Sort number positions left-to-right by x-coordinate.
99
+ sorted_positions = sorted(number_positions, key=lambda x: x[1][0])
100
+ labeled_positions = []
101
+ for i, (_, position) in enumerate(sorted_positions):
102
+ if i < len(number_values):
103
+ labeled_positions.append((number_values[i], position))
104
+
105
+ # Compute needle tip as midpoint between corner 3 and corner 4.
106
+ needle_tip_x = (needle_corners[2][0] + needle_corners[3][0]) / 2
107
+ needle_tip_y = (needle_corners[2][1] + needle_corners[3][1]) / 2
108
+ needle_tip = np.array([needle_tip_x, needle_tip_y])
109
+
110
+ # Check if needle tip exactly matches a number position.
111
+ for value, position in labeled_positions:
112
+ distance = np.sqrt((needle_tip[0] - position[0])**2 + (needle_tip[1] - position[1])**2)
113
+ if distance < 15: # threshold for "exact match"
114
+ return value, "exact_midpoint"
115
+
116
+ # If not an exact match, find the two numbers between which the needle lies.
117
+ left_value = None
118
+ right_value = None
119
+ left_position = None
120
+ right_position = None
121
+ for i in range(len(labeled_positions) - 1):
122
+ curr_value, curr_pos = labeled_positions[i]
123
+ next_value, next_pos = labeled_positions[i + 1]
124
+ if curr_pos[0] <= needle_tip[0] <= next_pos[0]:
125
+ left_value = curr_value
126
+ right_value = next_value
127
+ left_position = curr_pos
128
+ right_position = next_pos
129
+ break
130
+
131
+ # If not between any two, return the closest.
132
+ if left_value is None or right_value is None:
133
+ min_distance = float('inf')
134
+ closest_value = None
135
+ for value, position in labeled_positions:
136
+ distance = np.sqrt((needle_tip[0] - position[0])**2 + (needle_tip[1] - position[1])**2)
137
+ if distance < min_distance:
138
+ min_distance = distance
139
+ closest_value = value
140
+ return closest_value, "closest_midpoint"
141
+
142
+ # Interpolate based on x-distance.
143
+ total_x_distance = right_position[0] - left_position[0]
144
+ needle_x_distance = needle_tip[0] - left_position[0]
145
+ ratio = needle_x_distance / total_x_distance if total_x_distance > 0 else 0
146
+ value_range = right_value - left_value
147
+ interpolated_value = left_value + (ratio * value_range)
148
+ interpolated_value = round(interpolated_value, 1)
149
+
150
+ return interpolated_value, "interpolated_midpoint"
151
+
152
+ def process_meter_reading(analog_reading_model, image):
153
+ """
154
+ Run detection on the provided (cropped) meter image using analog_reading_v2.pt,
155
+ compute the meter reading, and print the result.
156
+ """
157
+ model = YOLO(analog_reading_model)
158
+ results = model(image)
159
+
160
+ needle_corners = None
161
+ number_positions = [] # Each element is a tuple: (detected_label, center)
162
+
163
+ # Process each detection result.
164
+ for r in results:
165
+ if hasattr(r, "obb") and r.obb is not None:
166
+ image = draw_obb(image, r.obb)
167
+ boxes = r.obb.xyxyxyxy.cpu().numpy()
168
+ classes = r.obb.cls.cpu().numpy()
169
+
170
+ for box, class_id in zip(boxes, classes):
171
+ class_name = r.names[int(class_id)]
172
+ center = get_center_point(box)
173
+ cv2.circle(image, (int(center[0]), int(center[1])), 3, (0, 0, 255), -1)
174
+
175
+ if class_name.lower() == "needle":
176
+ needle_corners = box.reshape(4, 2)
177
+ # Check if class is a digit (or the word "numbers") representing meter numbers.
178
+ elif class_name.isdigit() or class_name in ["0", "5", "10", "15", "20", "25", "30"] or class_name.lower() == "numbers":
179
+ number_positions.append((0, center))
180
+
181
+ # Label the numbers (using standard ordering) on the image.
182
+ if number_positions:
183
+ number_values = [0, 5, 10, 15, 20, 25, 30]
184
+ sorted_positions = sorted(number_positions, key=lambda x: x[1][0])
185
+ for i, (_, position) in enumerate(sorted_positions):
186
+ if i < len(number_values):
187
+ label = str(number_values[i])
188
+ cv2.putText(image, label,
189
+ (int(position[0]), int(position[1]) - 15),
190
+ cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
191
+
192
+ # Compute and print the meter reading if needle and numbers are detected.
193
+ if needle_corners is not None and number_positions:
194
+ needle_tip_x = (needle_corners[2][0] + needle_corners[3][0]) / 2
195
+ needle_tip_y = (needle_corners[2][1] + needle_corners[3][1]) / 2
196
+ needle_tip = np.array([needle_tip_x, needle_tip_y])
197
+
198
+ reading, method = calculate_meter_reading(needle_corners, number_positions)
199
+ if reading is not None:
200
+ result_text = f"Meter reading: {reading} ({method})"
201
+ print(result_text)
202
+
203
+ # Visualize connection between the needle tip and the nearest number.
204
+ number_values = [0, 5, 10, 15, 20, 25, 30]
205
+ sorted_positions = sorted(number_positions, key=lambda x: x[1][0])
206
+ labeled_positions = []
207
+ for i, (_, position) in enumerate(sorted_positions):
208
+ if i < len(number_values):
209
+ labeled_positions.append((number_values[i], position))
210
+
211
+ # Find adjacent numbers for interpolation visualization.
212
+ left_pos = None
213
+ right_pos = None
214
+ for i in range(len(labeled_positions) - 1):
215
+ curr_value, curr_pos = labeled_positions[i]
216
+ next_value, next_pos = labeled_positions[i + 1]
217
+ if curr_pos[0] <= needle_tip[0] <= next_pos[0]:
218
+ left_pos = curr_pos
219
+ right_pos = next_pos
220
+ break
221
+
222
+ if "interpolated" in method and left_pos is not None and right_pos is not None:
223
+ cv2.line(image,
224
+ (int(needle_tip[0]), int(needle_tip[1])),
225
+ (int(left_pos[0]), int(left_pos[1])),
226
+ (255, 0, 255), 1, cv2.LINE_AA)
227
+ cv2.line(image,
228
+ (int(needle_tip[0]), int(needle_tip[1])),
229
+ (int(right_pos[0]), int(right_pos[1])),
230
+ (255, 0, 255), 1, cv2.LINE_AA)
231
+ else:
232
+ # Connect to closest number if not interpolated.
233
+ min_distance = float('inf')
234
+ closest_position = None
235
+ for _, position in labeled_positions:
236
+ distance = np.sqrt((needle_tip[0] - position[0])**2 +
237
+ (needle_tip[1] - position[1])**2)
238
+ if distance < min_distance:
239
+ min_distance = distance
240
+ closest_position = position
241
+ if closest_position is not None:
242
+ cv2.line(image,
243
+ (int(needle_tip[0]), int(needle_tip[1])),
244
+ (int(closest_position[0]), int(closest_position[1])),
245
+ (255, 0, 255), 2)
246
+ else:
247
+ print("Needle position is out of range")
248
+ else:
249
+ if needle_corners is None:
250
+ print("Needle not detected")
251
+ if not number_positions:
252
+ print("No numbers detected")
253
+
254
+ return image
app.py ADDED
@@ -0,0 +1,243 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, File, UploadFile, Form, HTTPException
2
+ import cv2
3
+ import numpy as np
4
+ import os
5
+ from ultralytics import YOLO
6
+ from PIL import Image
7
+ import io
8
+ import easyocr
9
+ from ocr import detect_and_crop as ocr_detect_and_crop, detect_final_classes
10
+ from Remaining_test import draw_obb
11
+ from analog import crop_region, calculate_meter_reading, get_center_point, process_meter_reading, detect_and_crop_region
12
+ from fastapi.responses import Response
13
+ import tempfile
14
+
15
+ app = FastAPI()
16
+
17
+ try:
18
+ res_temp_box = YOLO("Models/res_temp_box.pt")
19
+ res_temp_ocr = YOLO("Models/res_temp_ocr.pt")
20
+ analog_box = YOLO("Models/analog_box_v2.pt")
21
+ analog_reading = YOLO("Models/analog_reading_v2.pt")
22
+ remaining_test_model = YOLO("Models/Remaining_tests_model.pt")
23
+ new_apparatus_model = YOLO("Models/New_Apparatus_model.pt")
24
+ except Exception as e:
25
+ print(f"Error loading models: {str(e)}")
26
+ raise
27
+
28
+ reader = easyocr.Reader(['en'])
29
+
30
+ def process_res_temp(file_bytes):
31
+ try:
32
+ # Try to process using both models and select the best result
33
+ image = Image.open(io.BytesIO(file_bytes)).convert("RGB")
34
+
35
+ # For OCR model processing (original res_temp approach)
36
+ cropped_regions = ocr_detect_and_crop(res_temp_box, image)
37
+ final_classes_dict = detect_final_classes(res_temp_ocr, cropped_regions)
38
+
39
+ # Convert image for apparatus model
40
+ image_cv = cv2.imdecode(np.frombuffer(file_bytes, np.uint8), cv2.IMREAD_COLOR)
41
+
42
+ # Process with new apparatus model
43
+ apparatus_results = new_apparatus_model(image_cv)
44
+ apparatus_data = {}
45
+ confidence_scores = {}
46
+
47
+ # Extract text using apparatus model
48
+ for r in apparatus_results:
49
+ if r.obb is not None:
50
+ # Get confidence scores for detections
51
+ confidences = r.obb.conf.cpu().numpy() if hasattr(r.obb, 'conf') else None
52
+
53
+ _, extracted_texts = draw_obb(image_cv.copy(), r.obb)
54
+ for i, class_id in enumerate(r.obb.cls.cpu().numpy()):
55
+ class_name = r.names[int(class_id)]
56
+ if i < len(extracted_texts) and extracted_texts[i]:
57
+ apparatus_data[class_name] = extracted_texts[i]
58
+
59
+ # Store confidence score if available
60
+ if confidences is not None and i < len(confidences):
61
+ confidence_scores[class_name] = float(confidences[i])
62
+ else:
63
+ confidence_scores[class_name] = 0.75 # Default fallback
64
+
65
+ # Combine results from both models
66
+ final_data = {**final_classes_dict, **apparatus_data}
67
+
68
+ # Calculate overall confidence (average of available scores)
69
+ overall_confidence = 0.0
70
+ if confidence_scores:
71
+ overall_confidence = sum(confidence_scores.values()) / len(confidence_scores)
72
+ else:
73
+ overall_confidence = 0.75 # Default if no scores available
74
+
75
+ # Round overall confidence to 2 decimal places
76
+ overall_confidence = round(overall_confidence, 2)
77
+
78
+ # Convert to key-value list format with individual confidence scores
79
+ kv_list = []
80
+ for k, v in final_data.items():
81
+ # Use the confidence score if available, otherwise use default
82
+ conf = round(confidence_scores.get(k, 0.75), 2)
83
+ kv_list.append({
84
+ "keyName": k,
85
+ "keyValue": "".join(v) if isinstance(v, list) else v,
86
+ "actualValue": "".join(v) if isinstance(v, list) else v,
87
+ "confidenceScore": conf
88
+ })
89
+
90
+ return {"ocs": overall_confidence, "extractions": kv_list}
91
+ except Exception as e:
92
+ raise HTTPException(status_code=400, detail=f"Error processing data: {str(e)}")
93
+
94
+
95
+ def process_remaining_test(file_bytes, expected_classes):
96
+ try:
97
+ image_cv = cv2.imdecode(np.frombuffer(file_bytes, np.uint8), cv2.IMREAD_COLOR)
98
+ if image_cv is None:
99
+ raise HTTPException(status_code=400, detail="Invalid image data for processing")
100
+
101
+ # Run inference using the remaining tests model
102
+ results = remaining_test_model(image_cv)
103
+
104
+ extracted_data = {}
105
+ confidence_scores = {}
106
+
107
+ # Process results and extract data with confidence scores
108
+ for r in results:
109
+ if r.obb is not None:
110
+ _, extracted_texts = draw_obb(image_cv.copy(), r.obb)
111
+ confidences = r.obb.conf.cpu().numpy()
112
+
113
+ for i, (class_id, conf) in enumerate(zip(r.obb.cls.cpu().numpy(), confidences)):
114
+ class_name = r.names[int(class_id)]
115
+ if class_name in expected_classes and i < len(extracted_texts) and extracted_texts[i]:
116
+ extracted_data[class_name] = extracted_texts[i]
117
+ confidence_scores[class_name] = float(conf)
118
+
119
+ # Calculate overall confidence with fallback
120
+ if confidence_scores:
121
+ overall_confidence = sum(confidence_scores.values()) / len(confidence_scores)
122
+ overall_confidence = round(overall_confidence, 2)
123
+ else:
124
+ overall_confidence = 0.0 # Fallback when no confidences available
125
+
126
+ # Create result list with proper error handling
127
+ result_list = [
128
+ {
129
+ "keyName": k,
130
+ "keyValue": round(float(v), 2) if isinstance(v, (int, float, str)) and str(v).replace('.','',1).isdigit() else v,
131
+ "actualValue": round(float(v), 2) if isinstance(v, (int, float, str)) and str(v).replace('.','',1).isdigit() else v,
132
+ "confidenceScore": round(confidence_scores.get(k, overall_confidence), 2)
133
+ }
134
+ for k, v in extracted_data.items()
135
+ if v is not None # Skip None values
136
+ ]
137
+
138
+ if not result_list:
139
+ raise HTTPException(
140
+ status_code=400,
141
+ detail=f"No valid data found for expected classes: {expected_classes}"
142
+ )
143
+
144
+ return {"ocs": overall_confidence, "extractions": result_list}
145
+
146
+ except Exception as e:
147
+ raise HTTPException(status_code=400, detail=f"Error processing test data: {str(e)}")
148
+
149
+
150
+ def process_dc_test(file_bytes):
151
+ """
152
+ Implements the DC_TEST pipeline using functions from analog.py.
153
+ It decodes the image, ensures consistent color format, detects and crops the meter
154
+ region using the analog_box model, and then uses the analog_reading model along with
155
+ calculate_meter_reading and get_center_point to compute the meter reading.
156
+ """
157
+ try:
158
+ # Decode file bytes into a CV image (BGR)
159
+ image_cv = cv2.imdecode(np.frombuffer(file_bytes, np.uint8), cv2.IMREAD_COLOR)
160
+ if image_cv is None:
161
+ raise HTTPException(status_code=400, detail="Invalid image data for DC_TEST")
162
+
163
+ results = analog_box(image_cv)
164
+ cropped_meter = None
165
+ for r in results:
166
+ if hasattr(r, "obb") and r.obb is not None:
167
+ cropped_meter = crop_region(image_cv, r.obb)
168
+ if cropped_meter is not None:
169
+ break
170
+
171
+ if cropped_meter is None:
172
+ raise HTTPException(status_code=400, detail="No analog meter detected in image")
173
+
174
+ meter_results = analog_reading(cropped_meter)
175
+ needle_corners = None
176
+ needle_corners = None
177
+ number_positions = []
178
+ needle_confidence = 0
179
+ number_confidences = []
180
+
181
+ for r in meter_results:
182
+ if hasattr(r, "obb") and r.obb is not None:
183
+ boxes = r.obb.xyxyxyxy.cpu().numpy()
184
+ classes = r.obb.cls.cpu().numpy()
185
+ confidences = r.obb.conf.cpu().numpy() # Get confidence scores
186
+
187
+ for box, class_id, conf in zip(boxes, classes, confidences):
188
+ class_name = r.names[int(class_id)]
189
+ center = get_center_point(box)
190
+
191
+ if class_name.lower() == "needle":
192
+ needle_corners = box.reshape(4, 2)
193
+ needle_confidence = float(conf)
194
+ elif (class_name.isdigit() or
195
+ class_name in ["0", "5", "10", "15", "20", "25", "30"] or
196
+ class_name.lower() == "numbers"):
197
+ number_positions.append((0, center))
198
+ number_confidences.append(float(conf))
199
+
200
+ if needle_corners is not None and number_positions:
201
+ reading, method = calculate_meter_reading(needle_corners, number_positions)
202
+
203
+ # Calculate overall confidence using the formula
204
+ # (2 * needle_confidence + sum of number confidences) / (2 + number of numbers)
205
+ overall_confidence = (2 * needle_confidence + sum(number_confidences)) / (2 + len(number_confidences))
206
+ overall_confidence = round(overall_confidence, 2)
207
+
208
+ reading = round(float(reading), 2)
209
+
210
+ list = [{
211
+ "keyName": "MeterReading",
212
+ "keyValue": str(reading),
213
+ "actualValue": str(reading),
214
+ "confidenceScore": overall_confidence,
215
+ }]
216
+
217
+ return {
218
+ "ocs": overall_confidence,
219
+ "extractions": list
220
+ }
221
+
222
+ except Exception as e:
223
+ raise HTTPException(status_code=400, detail=f"Error processing DC_TEST: {str(e)}")
224
+
225
+
226
+ @app.post("/detect/")
227
+ async def detect(file: UploadFile = File(...), test_type: str = Form(...)):
228
+ file_bytes = await file.read()
229
+ if test_type == "CONDUCTOR_RESISTANCE_TEST":
230
+ return process_res_temp(file_bytes)
231
+ elif test_type == "DC_TEST":
232
+ # For DC_TEST, use the enhanced pipeline which now calls analog_reading only once.
233
+ return process_dc_test(file_bytes)
234
+ elif test_type == "PARTIAL_DISCHARGE_TEST":
235
+ return process_remaining_test(file_bytes, expected_classes=["UVolt", "qCValue"])
236
+ elif test_type == "HIGH_VOLTAGE_TEST":
237
+ return process_remaining_test(file_bytes, expected_classes=["kV", "TimeLeft", "q(IEC) value"])
238
+ else:
239
+ raise HTTPException(status_code=400, detail="Invalid test_type. Choose 'CONDUCTOR_RESISTANCE_TEST', 'DC_TEST', 'PARTIAL_DISCHARGE_TEST', or 'HIGH_VOLTAGE_TEST'")
240
+
241
+ @app.get("/")
242
+ def health_check():
243
+ return {"status": "healthy", "version": "v2.4"}
docker-compose.yaml ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ version: "3.8"
2
+
3
+ services:
4
+ fastapi-app:
5
+ build: .
6
+ ports:
7
+ - "8000:8000"
8
+ volumes:
9
+ - .:/app
10
+ restart: always
new_apparatus.py ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import cv2
3
+ import numpy as np
4
+ import easyocr
5
+ from ultralytics import YOLO
6
+
7
+ # Initialize EasyOCR reader
8
+ reader = easyocr.Reader(['en'])
9
+
10
+ def draw_obb(image, obb):
11
+ boxes = obb.xyxyxyxy.cpu().numpy()
12
+ extracted_texts = []
13
+
14
+ for i, box in enumerate(boxes):
15
+ pts = box.reshape(4, 2).astype(np.int32)
16
+
17
+ # Draw the bounding box
18
+ cv2.polylines(image, [pts], isClosed=True, color=(0, 255, 0), thickness=2)
19
+
20
+ # Crop the detected region
21
+ x_min, y_min = np.min(pts, axis=0)
22
+ x_max, y_max = np.max(pts, axis=0)
23
+ cropped_region = image[y_min:y_max, x_min:x_max]
24
+
25
+ # Apply OCR on the cropped region
26
+ if cropped_region.size > 0:
27
+ text_results = reader.readtext(cropped_region)
28
+ detected_text = " ".join([text[1] for text in text_results])
29
+ extracted_texts.append(detected_text)
30
+
31
+ # Put extracted text on the image
32
+ cv2.putText(image, detected_text, (x_min, y_min - 10),
33
+ cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 255), 2)
34
+
35
+ return image, extracted_texts
36
+
37
+ def main(model_path_3, image_path):
38
+ # Load the YOLO OBB model for detection
39
+ model_3 = YOLO(model_path_3)
40
+
41
+ # Read the input image
42
+ image = cv2.imread(image_path)
43
+ if image is None:
44
+ print("Error: Could not read image at", image_path)
45
+ sys.exit(1)
46
+
47
+ # Run inference using model_3 for detection
48
+ results = model_3(image)
49
+
50
+ # Iterate over the results and draw OBB predictions
51
+ for r in results:
52
+ if r.obb is not None:
53
+ image, extracted_texts = draw_obb(image, r.obb)
54
+ for i, class_id in enumerate(r.obb.cls.cpu().numpy()):
55
+ class_name = r.names[int(class_id)]
56
+ print(f"Detected class ID: {class_id}, Class name: {class_name}")
57
+
58
+ # Print extracted texts from OCR
59
+ for idx, text in enumerate(extracted_texts):
60
+ print(f"OCR Extracted Text {idx + 1}: {text}")
61
+
62
+ # Display the resulting image with bounding boxes and text
63
+ cv2.imshow("Detections with OCR", image)
64
+ cv2.waitKey(0)
65
+ cv2.destroyAllWindows()
66
+
67
+ if __name__ == "__main__":
68
+ model_path_3 = "Models/new apparatus.pt"
69
+ image_path = "test_images/1 (1).jpg"
70
+ main(model_path_3, image_path)
ocr.py ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import numpy as np
3
+ import matplotlib.pyplot as plt
4
+ from ultralytics import YOLO
5
+ from PIL import Image
6
+
7
+ def visualize_with_obb(image, obb):
8
+ """Visualizes OBB bounding boxes on the image using Matplotlib."""
9
+ fig, ax = plt.subplots(figsize=(8, 6))
10
+ ax.imshow(image)
11
+
12
+ if obb is not None:
13
+ boxes = obb.xyxyxyxy.cpu().numpy()
14
+ for box in boxes:
15
+ pts = box.reshape(4, 2) # Convert to (x, y) points
16
+ ax.plot([pts[i][0] for i in [0,1,2,3,0]],
17
+ [pts[i][1] for i in [0,1,2,3,0]],
18
+ linestyle='-', linewidth=2, color='lime')
19
+
20
+
21
+ def crop_regions(image, obb, class_ids, names):
22
+ """Crops regions based on OBB detection and returns (cropped image, x-coordinate, class_name)."""
23
+ cropped_regions = []
24
+
25
+ if obb is not None:
26
+ boxes = obb.xyxyxyxy.cpu().numpy()
27
+ for i, box in enumerate(boxes):
28
+ pts = box.reshape(4, 2).astype(int)
29
+ x_min, y_min = np.min(pts, axis=0)
30
+ x_max, y_max = np.max(pts, axis=0)
31
+
32
+ # Get class name for this box
33
+ class_id = int(class_ids[i])
34
+ class_name = names[class_id]
35
+
36
+ cropped = image.crop((x_min, y_min, x_max, y_max))
37
+ cropped_regions.append((cropped, x_min, class_name)) # Store class_name with cropped image
38
+
39
+ return cropped_regions
40
+
41
+ def detect_and_crop(model_3, image):
42
+ """Runs first detection model (OBB-based) and returns cropped regions with class info."""
43
+ results = model_3(image)
44
+ cropped_regions = []
45
+
46
+ for r in results:
47
+ visualize_with_obb(image, r.obb) # Display first detection
48
+ if r.obb is not None:
49
+ cropped_regions = crop_regions(image, r.obb, r.obb.cls.cpu().numpy(), r.names)
50
+
51
+ return cropped_regions
52
+
53
+ def detect_final_classes(model_4, cropped_regions):
54
+ """Runs second detection model (OBB-based OCR) and returns detected classes by first class."""
55
+ class_results = {} # Dictionary to store results by class
56
+
57
+ for cropped, x_min, class_name in cropped_regions:
58
+ results = model_4(cropped)
59
+ detected_data = []
60
+
61
+ for r in results:
62
+ if r.obb is not None:
63
+ for i, class_id in enumerate(r.obb.cls.cpu().numpy()):
64
+ ocr_class_name = r.names[int(class_id)]
65
+ box_pts = r.obb.xyxyxyxy.cpu().numpy()[i].reshape(4, 2)
66
+ x_center = np.mean(box_pts[:, 0])
67
+
68
+ detected_data.append((ocr_class_name, x_center))
69
+
70
+ # Sort detected characters by x-center (left to right)
71
+ detected_data.sort(key=lambda x: x[1])
72
+
73
+ # Process to place '.' after second digit if needed
74
+ final_classes = [
75
+ "." if cls == "dot" else "°" if cls == "degree" else cls
76
+ for cls, _ in detected_data
77
+ ]
78
+
79
+ # Store result by first detection class
80
+ if class_name not in class_results:
81
+ class_results[class_name] = []
82
+ class_results[class_name] = final_classes
83
+
84
+ return class_results
85
+
86
+ def main(model_path_3, model_path_4, image_path):
87
+ """Main function to run both detections using OBB models and visualize results."""
88
+ model_3 = YOLO(model_path_3)
89
+ model_4 = YOLO(model_path_4)
90
+
91
+ image = Image.open(image_path).convert("RGB")
92
+
93
+ # First detection to identify and crop regions by class
94
+ cropped_regions = detect_and_crop(model_3, image)
95
+
96
+ # Second detection to read values from each cropped region
97
+ class_results = detect_final_classes(model_4, cropped_regions)
98
+
99
+ # Display results for each class separately
100
+ print("Detection Results by Class:")
101
+ for class_name, values in class_results.items():
102
+ print(f" {class_name}: {''.join(values)}")
103
+
requirements.txt ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ annotated-types==0.7.0
2
+ certifi==2024.12.14
3
+ charset-normalizer==3.4.1
4
+ click==8.1.8
5
+ colorama==0.4.6
6
+ contourpy==1.1.1
7
+ cycler==0.12.1
8
+ easyocr==1.7.2
9
+ fastapi==0.95.2
10
+ filelock==3.16.1
11
+ fonttools==4.55.3
12
+ fsspec==2024.12.0
13
+ h11==0.14.0
14
+ idna==3.10
15
+ imageio==2.35.1
16
+ Jinja2==3.1.5
17
+ kiwisolver==1.4.7
18
+ lazy_loader==0.4
19
+ MarkupSafe==2.1.5
20
+ matplotlib==3.7.5
21
+ mpmath==1.3.0
22
+ networkx==3.1
23
+ ninja==1.11.1.3
24
+ numpy==1.24.4
25
+ opencv-python==4.11.0.86
26
+ opencv-python-headless==4.11.0.86
27
+ packaging==24.2
28
+ pandas==2.0.3
29
+ pillow==10.4.0
30
+ pipdeptree==2.24.0
31
+ psutil==6.1.1
32
+ py-cpuinfo==9.0.0
33
+ pyclipper==1.3.0.post6
34
+ pydantic==1.10.9
35
+ pydantic_core==2.27.2
36
+ pyparsing==3.1.4
37
+ pytesseract==0.3.13
38
+ python-bidi==0.6.3
39
+ python-dateutil==2.9.0.post0
40
+ python-multipart==0.0.20
41
+ pytz==2024.2
42
+ PyYAML==6.0.2
43
+ requests==2.32.3
44
+ scikit-image==0.21.0
45
+ scipy==1.10.1
46
+ seaborn==0.13.2
47
+ shapely==2.0.6
48
+ six==1.17.0
49
+ sniffio==1.3.1
50
+ starlette==0.27.0
51
+ sympy==1.13.1
52
+ tifffile==2023.7.10
53
+ torch==2.4.1
54
+ torchvision==0.19.1
55
+ tqdm==4.67.1
56
+ typing_extensions==4.12.2
57
+ tzdata==2024.2
58
+ ultralytics==8.3.65
59
+ ultralytics-thop==2.0.14
60
+ urllib3==2.2.3
61
+ uvicorn==0.33.0
62
+ easyocr==1.7.2