Spaces:
Runtime error
Runtime error
import sys | |
import cv2 | |
import numpy as np | |
from ultralytics import YOLO | |
import math | |
# ----------------------------- | |
# Part 1: Helper functions for cropping (unchanged) | |
# ----------------------------- | |
def draw_obb(image, obb): | |
"""Draw oriented bounding boxes on an image.""" | |
return image | |
def order_points(pts): | |
"""Order 4 points as top-left, top-right, bottom-right, bottom-left.""" | |
rect = np.zeros((4, 2), dtype="float32") | |
s = pts.sum(axis=1) | |
rect[0] = pts[np.argmin(s)] | |
rect[2] = pts[np.argmax(s)] | |
diff = np.diff(pts, axis=1) | |
rect[1] = pts[np.argmin(diff)] | |
rect[3] = pts[np.argmax(diff)] | |
return rect | |
def crop_region(image, obb): | |
"""Crop the meter region from the image using the OBB.""" | |
boxes = obb.xyxyxyxy.cpu().numpy() | |
if len(boxes) == 0: | |
return None | |
box = boxes[0] | |
pts = box.reshape(4, 2).astype(np.float32) | |
rect = cv2.minAreaRect(pts) | |
width = int(rect[1][0]) | |
height = int(rect[1][1]) | |
if width <= 0 or height <= 0: | |
return None | |
dst_pts = np.array([ | |
[0, 0], | |
[width - 1, 0], | |
[width - 1, height - 1], | |
[0, height - 1]], dtype=np.float32) | |
ordered_pts = order_points(pts) | |
M = cv2.getPerspectiveTransform(ordered_pts, dst_pts) | |
cropped = cv2.warpPerspective(image, M, (width, height)) | |
return cropped | |
def detect_and_crop_region(analog_box_model, image_path): | |
"""Detect the meter region using analog_box.pt and return the cropped image.""" | |
model = YOLO(analog_box_model) | |
image = cv2.imread(image_path) | |
if image is None: | |
return None | |
results = model(image) | |
for r in results: | |
if hasattr(r, "obb") and r.obb is not None: | |
cropped = crop_region(image, r.obb) | |
if cropped is not None: | |
return cropped | |
return None | |
# ----------------------------- | |
# Part 2: Enhanced needle corner coordinate ratio logic | |
# ----------------------------- | |
def get_center_point(box): | |
"""Calculate the center point of a bounding box (4 corners).""" | |
pts = box.reshape(4, 2) | |
center_x = np.mean(pts[:, 0]) | |
center_y = np.mean(pts[:, 1]) | |
return (center_x, center_y) | |
def calculate_distance(point1, point2): | |
"""Calculate Euclidean distance between two points.""" | |
return math.sqrt((point1[0] - point2[0])**2 + (point1[1] - point2[1])**2) | |
def find_needle_tip_corners(needle_corners): | |
""" | |
Extract needle tip coordinates from corners 3 and 4. | |
Returns both individual corners and their midpoint. | |
""" | |
corner3 = needle_corners[2] # Corner 3 | |
corner4 = needle_corners[3] # Corner 4 | |
# Calculate midpoint of corners 3 and 4 | |
tip_midpoint = np.array([ | |
(corner3[0] + corner4[0]) / 2, | |
(corner3[1] + corner4[1]) / 2 | |
]) | |
return corner3, corner4, tip_midpoint | |
def find_digit_boundaries(labeled_positions): | |
""" | |
Create boundaries between consecutive digits for range detection. | |
Returns list of (lower_value, upper_value, lower_pos, upper_pos) tuples. | |
""" | |
boundaries = [] | |
for i in range(len(labeled_positions) - 1): | |
lower_value, lower_pos = labeled_positions[i] | |
upper_value, upper_pos = labeled_positions[i + 1] | |
boundaries.append((lower_value, upper_value, lower_pos, upper_pos)) | |
return boundaries | |
def calculate_needle_corner_ratio_approximation(needle_corners, number_positions): | |
""" | |
Enhanced ratio approximation logic based on needle corners 3 and 4 coordinates. | |
""" | |
number_values = [0, 5, 10, 15, 20, 25, 30] | |
# Sort number positions left-to-right by x-coordinate | |
sorted_positions = sorted(number_positions, key=lambda x: x[1][0]) | |
labeled_positions = [] | |
for i, (_, position) in enumerate(sorted_positions): | |
if i < len(number_values): | |
labeled_positions.append((number_values[i], position)) | |
if len(labeled_positions) < 2: | |
return None, "insufficient_digits" | |
# Get needle tip corner coordinates | |
corner3, corner4, tip_midpoint = find_needle_tip_corners(needle_corners) | |
# Define proximity thresholds | |
EXACT_MATCH_THRESHOLD = 15 # Very close to digit | |
PROXIMITY_THRESHOLD = 50 # Within range for ratio calculation | |
# Check each needle corner position | |
needle_points = [ | |
("Corner 3", corner3), | |
("Corner 4", corner4), | |
("Tip Midpoint", tip_midpoint) | |
] | |
best_reading = None | |
best_method = None | |
best_confidence = 0 | |
for point_name, needle_point in needle_points: | |
# Find closest digit to this needle point | |
closest_distance = float('inf') | |
closest_digit = None | |
closest_position = None | |
for value, position in labeled_positions: | |
distance = calculate_distance(needle_point, position) | |
if distance < closest_distance: | |
closest_distance = distance | |
closest_digit = value | |
closest_position = position | |
# Check if needle point is very close to a digit (exact match) | |
if closest_distance < EXACT_MATCH_THRESHOLD: | |
confidence = 1.0 - (closest_distance / EXACT_MATCH_THRESHOLD) | |
if confidence > best_confidence: | |
best_reading = float(closest_digit) | |
best_method = f"exact_match_{point_name.lower().replace(' ', '_')}" | |
best_confidence = confidence | |
continue | |
# Check if needle point is within proximity for ratio approximation | |
if closest_distance < PROXIMITY_THRESHOLD: | |
# Find the digit range this needle point falls into | |
digit_boundaries = find_digit_boundaries(labeled_positions) | |
for lower_value, upper_value, lower_pos, upper_pos in digit_boundaries: | |
# Check if needle point is between these two digits | |
lower_dist = calculate_distance(needle_point, lower_pos) | |
upper_dist = calculate_distance(needle_point, upper_pos) | |
# Check if needle X coordinate is between the two digit X coordinates | |
if lower_pos[0] <= needle_point[0] <= upper_pos[0]: | |
# Calculate ratio based on X-coordinate position | |
x_range = upper_pos[0] - lower_pos[0] | |
x_offset = needle_point[0] - lower_pos[0] | |
x_ratio = x_offset / x_range if x_range > 0 else 0 | |
# Calculate ratio based on distance | |
total_distance = lower_dist + upper_dist | |
distance_ratio = upper_dist / total_distance if total_distance > 0 else 0 | |
# Combine ratios for better accuracy | |
combined_ratio = (x_ratio + distance_ratio) / 2 | |
# Calculate interpolated value | |
value_range = upper_value - lower_value | |
interpolated_value = lower_value + (combined_ratio * value_range) | |
# Check which digit the needle is closer to | |
if lower_dist < upper_dist: | |
closer_digit = lower_value | |
closer_distance = lower_dist | |
else: | |
closer_digit = upper_value | |
closer_distance = upper_dist | |
# If very close to the closer digit, return that digit | |
if closer_distance < EXACT_MATCH_THRESHOLD * 1.5: # Slightly more lenient | |
confidence = 1.0 - (closer_distance / (EXACT_MATCH_THRESHOLD * 1.5)) | |
if confidence > best_confidence: | |
best_reading = float(closer_digit) | |
best_method = f"close_to_digit_{point_name.lower().replace(' ', '_')}" | |
best_confidence = confidence | |
else: | |
# Use ratio approximation | |
confidence = 1.0 - (min(lower_dist, upper_dist) / PROXIMITY_THRESHOLD) | |
if confidence > best_confidence: | |
best_reading = round(interpolated_value, 1) | |
best_method = f"ratio_approximation_{point_name.lower().replace(' ', '_')}" | |
best_confidence = confidence | |
break | |
# Return the best result found | |
if best_reading is not None: | |
return best_reading, best_method | |
# Fallback: use closest digit to tip midpoint | |
closest_distance = float('inf') | |
closest_value = None | |
for value, position in labeled_positions: | |
distance = calculate_distance(tip_midpoint, position) | |
if distance < closest_distance: | |
closest_distance = distance | |
closest_value = value | |
return float(closest_value), "fallback_closest" | |
def process_meter_reading_with_corner_logic(analog_reading_model, image): | |
""" | |
Process meter reading using the enhanced needle corner coordinate logic. | |
""" | |
model = YOLO(analog_reading_model) | |
results = model(image) | |
needle_corners = None | |
number_positions = [] | |
# Process each detection result | |
for r in results: | |
if hasattr(r, "obb") and r.obb is not None: | |
boxes = r.obb.xyxyxyxy.cpu().numpy() | |
classes = r.obb.cls.cpu().numpy() | |
for box, class_id in zip(boxes, classes): | |
class_name = r.names[int(class_id)] | |
center = get_center_point(box) | |
if class_name.lower() == "needle": | |
needle_corners = box.reshape(4, 2) | |
elif class_name.isdigit() or class_name in ["0", "5", "10", "15", "20", "25", "30"] or class_name.lower() == "numbers": | |
number_positions.append((0, center)) | |
# Calculate meter reading using enhanced corner logic | |
if needle_corners is not None and number_positions: | |
reading, method = calculate_needle_corner_ratio_approximation(needle_corners, number_positions) | |
if reading is not None: | |
print(f"Meter Reading: {reading}") | |
return image | |
# ----------------------------- | |
# Main Integration | |
# ----------------------------- | |
def main(): | |
# Paths for models and the input image | |
analog_box_model = "Models/analog_box_v2.pt" | |
analog_reading_model = "Models/analog_reading_v2.pt" | |
full_image_path = "extracted_frames/265.png" | |
# Step 1: Detect and crop the meter region | |
cropped_meter = detect_and_crop_region(analog_box_model, full_image_path) | |
if cropped_meter is None: | |
return | |
# Step 2: Process with needle corner coordinate logic | |
processed_image = process_meter_reading_with_corner_logic(analog_reading_model, cropped_meter) | |
if __name__ == "__main__": | |
main() |