afms-backend / src /utils /match_spec.py
ShaikFayaz042
Added backend code: app.py, requirements.txt, models, etc.
70ebc90
Raw
History Blame Contribute Delete
21.8 kB
from pathlib import Path
import csv
import os
from typing import Dict, Tuple, List, Any
# Get project root
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
# ============== BOLT STANDARDS (ISO 4017) ==============
BOLT_STANDARDS: Dict[str, Dict[str, Any]] = {
'M3': {'diameter': 3.0, 'preferred_lengths': [6, 8, 10, 12, 16, 20, 25, 30]},
'M4': {'diameter': 4.0, 'preferred_lengths': [8, 10, 12, 16, 20, 25, 30, 35, 40]},
'M5': {'diameter': 5.0, 'preferred_lengths': [10, 12, 16, 20, 25, 30, 35, 40, 45, 50]},
'M6': {'diameter': 6.0, 'preferred_lengths': [12, 16, 20, 25, 30, 35, 40, 45, 50, 55, 60]},
'M8': {'diameter': 8.0, 'preferred_lengths': [16, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 80]},
'M10': {'diameter': 10.0, 'preferred_lengths': [20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 80, 90, 100]},
'M12': {'diameter': 12.0, 'preferred_lengths': [24, 30, 35, 40, 45, 50, 55, 60, 65, 70, 80, 90, 100, 110, 120]},
'M16': {'diameter': 16.0, 'preferred_lengths': [32, 40, 50, 60, 70, 80, 100, 120, 150]},
'M20': {'diameter': 20.0, 'preferred_lengths': [40, 50, 60, 70, 80, 100, 120, 150]},
'M24': {'diameter': 24.0, 'preferred_lengths': [48, 60, 70, 80, 100, 120, 150]},
}
# ---------------- Helper Functions ----------------
def read_measurement_txt(file_path):
"""Read a measurement txt file and return dictionary of values."""
data = {}
with open(file_path, 'r') as f:
for line in f:
if ':' in line:
key, val = line.strip().split(':')
try:
data[key.strip()] = float(val.strip())
except:
data[key.strip()] = val.strip()
return data
def read_reference_csv(csv_path, obj_class):
"""Read reference CSV for a specific class and return a list of dicts."""
ref_list = []
with open(csv_path, 'r') as f:
reader = csv.DictReader(f)
for row in reader:
if obj_class == "washer":
ref_list.append({
"Nominal Dia": row["Nominal Dia (mm)"],
"Bolt Dia": float(row["Bolt Dia (mm)"]),
"ID_mm": float(row["Inner Dia (mm)"]),
"OD_mm": float(row["Outer Dia (mm)"]),
"Thickness_mm": float(row["Thickness (mm)"])
})
elif obj_class == "bolt":
# Store Across Corners (mm) as AC_mm for matching
# Length_mm is measured from image and compared with dataset
ref_list.append({
"Nominal Dia": row["Nominal Dia (mm)"],
"Bolt Size": row["Bolt Size"],
"Length_mm": float(row["Length (mm)"]),
"AF_mm": float(row["Across Flats (mm)"]),
"AC_mm": float(row["Across Corners (mm)"]),
"Head_Thickness_mm": float(row["Head Thickness (mm)"]),
"Fine_Pitch_mm": float(row["Standard Fine Pitch (mm)"]),
"Rough_Pitch_mm": float(row["Standard Rough Pitch (mm)"])
})
elif obj_class == "nut":
# Update keys as per your nuts CSV
ref_list.append({
"Nominal Dia": row["Nominal Dia (mm)"],
"Nut Size": row["Nut Size"],
"AF_mm": float(row["Across Flats (mm)"]),
"AC_mm": float(row["Across Corners (mm)"]),
"Thickness_mm": float(row["Thickness (mm)"])
})
elif obj_class == "screw":
# Update keys as per your screws CSV
ref_list.append({
"Nominal Dia": row["Nominal Dia (mm)"],
"Screw Size": row["Screw Size"],
"Length_mm": float(row["Length (mm)"]),
"Diameter_mm": float(row["Shank Dia (mm)"]),
"Head_Dia_mm": float(row["Head Dia (mm)" ]),
"Thread_Pitch_mm": float(row["Thread Pitch (mm)" ]),
"Head_Thickness_mm": float(row["Head Thickness (mm)"])
})
return ref_list
def calculate_bolt_length_range(diameter: float) -> Tuple[float, float]:
"""
Calculate min and max standard lengths for a bolt diameter per ISO 4017.
Min Length = 2 × diameter
Max Length = min(10 × diameter, 150mm)
Args:
diameter (float): Bolt diameter in mm (e.g., 6.0 for M6)
Returns:
Tuple[float, float]: (min_length_mm, max_length_mm)
Example:
>>> calculate_bolt_length_range(6.0)
(12.0, 60.0)
>>> calculate_bolt_length_range(24.0)
(48.0, 150.0)
"""
try:
if diameter <= 0:
return (0.0, 0.0)
min_length: float = 2 * diameter
max_length: float = min(10 * diameter, 150.0)
return (min_length, max_length)
except (TypeError, ValueError) as e:
print(f"⚠️ Error calculating bolt length range: {e}")
return (0.0, 0.0)
def snap_to_standard_length(measured_length: float,
min_length: float,
max_length: float,
preferred_lengths: List[int]) -> Dict[str, Any]:
"""
Find the closest standard length within the valid range per ISO 4017.
Args:
measured_length (float): Measured bolt length in mm
min_length (float): Minimum allowed length in mm
max_length (float): Maximum allowed length in mm
preferred_lengths (List[int]): List of preferred standard lengths
Returns:
Dict containing:
- standard_length (int): Snapped standard length
- deviation (float): Difference from measured
- valid (bool): Length within valid range
- in_range_standards (List[int]): Available standards within range
Example:
>>> snap_to_standard_length(18.78, 12, 60, [12,16,20,25,30,35,40,45,50,55,60])
{'standard_length': 20, 'deviation': 1.22, 'valid': True, ...}
"""
try:
# Validate inputs
if not isinstance(preferred_lengths, (list, tuple)) or len(preferred_lengths) == 0:
return {
'standard_length': None,
'deviation': None,
'valid': False,
'error': 'Empty or invalid preferred_lengths list'
}
if measured_length < 0 or min_length < 0 or max_length < 0:
return {
'standard_length': None,
'deviation': None,
'valid': False,
'error': 'Negative length values'
}
# Filter standards within valid range
in_range_standards: List[int] = [
length for length in preferred_lengths
if min_length <= length <= max_length
]
# If no standards in range, return closest boundary
if len(in_range_standards) == 0:
if measured_length < min_length:
standard_length = min_length
else:
standard_length = max_length
return {
'standard_length': int(standard_length),
'deviation': float(measured_length - standard_length),
'valid': False,
'in_range_standards': in_range_standards,
'error': 'Measured length outside valid range'
}
# Find closest standard length
closest_standard = min(
in_range_standards,
key=lambda x: abs(float(x) - measured_length)
)
deviation: float = measured_length - closest_standard
is_valid: bool = min_length <= measured_length <= max_length
return {
'standard_length': int(closest_standard),
'deviation': float(round(deviation, 2)),
'valid': is_valid,
'in_range_standards': in_range_standards,
'error': None
}
except (TypeError, ValueError, AttributeError) as e:
print(f"⚠️ Error snapping to standard length: {e}")
return {
'standard_length': None,
'deviation': None,
'valid': False,
'error': str(e)
}
def compare_with_reference(measured, ref_list, tolerance=0.5):
"""
Compare measured values with reference list.
Returns best match (min deviation) and status.
"""
best_match = None
min_total_dev = float('inf')
# Special logic for bolt: match measured AC_mm to AC_mm (Across Corners)
if ref_list and 'AC_mm' in ref_list[0] and 'AC_mm' in measured:
for ref in ref_list:
dev = abs(measured['AC_mm'] - ref['AC_mm'])
if dev < min_total_dev:
min_total_dev = dev
best_match = ref
# If best match found, apply ISO 4017 standard validation
if best_match:
# Step 1: Get bolt size from best match
bolt_size = best_match.get('Bolt Size', '')
# Step 2: Calculate ISO standard range for this bolt size
diameter = best_match.get('Nominal Dia', 0)
min_length, max_length = calculate_bolt_length_range(float(diameter))
# Step 3: Get measured shaft length (total length - head thickness)
head_thickness = best_match.get('Head_Thickness_mm', 0)
measured_total_length = measured.get('Length_mm', 0)
actual_shaft_length = measured_total_length - head_thickness
measured['Actual_Length_mm'] = actual_shaft_length
# Step 4: Snap to nearest standard length
if bolt_size in BOLT_STANDARDS:
preferred_lengths = BOLT_STANDARDS[bolt_size]['preferred_lengths']
else:
# Fallback: use generic standards if bolt size not in dict
preferred_lengths = [int(x) for x in range(int(min_length), int(max_length) + 1, 5)]
snap_result = snap_to_standard_length(
actual_shaft_length,
min_length,
max_length,
preferred_lengths
)
# Step 5: Store validation results
if snap_result.get('error') is None:
best_match['Standard_Length_mm'] = snap_result['standard_length']
best_match['Length_Deviation_mm'] = snap_result['deviation']
best_match['Length_Valid'] = snap_result['valid']
best_match['Length_In_Range'] = snap_result['valid']
else:
best_match['Standard_Length_mm'] = actual_shaft_length
best_match['Length_Deviation_mm'] = 0.0
best_match['Length_Valid'] = False
best_match['Length_In_Range'] = False
# Store bolt standards info for reporting
best_match['Min_Standard_Length'] = min_length
best_match['Max_Standard_Length'] = max_length
best_match['Preferred_Standards'] = preferred_lengths[:7] # Show first 7 standards
# Replace original Length_mm with snapped Standard_Length_mm for consistent output
best_match['Length_mm'] = best_match['Standard_Length_mm']
# Prepare deviation report
deviation = {'AC_mm': measured['AC_mm'] - best_match['AC_mm']} if best_match else {}
if best_match and 'Actual_Length_mm' in measured:
deviation['Length_mm'] = measured['Actual_Length_mm'] - best_match.get('Standard_Length_mm', 0)
status = 'pass' if best_match is not None else 'fail'
return best_match, deviation, status
# Special logic for screw: match measured Head_Dia_mm to Head_Dia_mm,
# then compute actual shaft length = total length - head thickness and snap to a standard length.
if ref_list and 'Head_Dia_mm' in ref_list[0] and 'Head_Dia_mm' in measured:
for ref in ref_list:
dev = abs(measured['Head_Dia_mm'] - ref['Head_Dia_mm'])
if dev < min_total_dev:
min_total_dev = dev
best_match = ref
if best_match:
measured_total_length = measured.get('Length_mm', 0)
head_thickness = best_match.get('Head_Thickness_mm', 0)
actual_shaft_length = measured_total_length - head_thickness
measured['Actual_Length_mm'] = actual_shaft_length
# Use available lengths for this screw size from the dataset.
screw_size = best_match.get('Screw Size')
preferred_lengths = sorted({
float(ref_item['Length_mm'])
for ref_item in ref_list
if ref_item.get('Screw Size') == screw_size
})
if not preferred_lengths:
preferred_lengths = sorted({float(ref_item['Length_mm']) for ref_item in ref_list})
if preferred_lengths:
closest_standard = min(preferred_lengths, key=lambda x: abs(actual_shaft_length - x))
min_length = min(preferred_lengths)
max_length = max(preferred_lengths)
length_valid = min_length <= actual_shaft_length <= max_length
best_match['Standard_Length_mm'] = float(closest_standard)
best_match['Length_Deviation_mm'] = float(round(actual_shaft_length - closest_standard, 2))
best_match['Length_Valid'] = length_valid
best_match['Length_In_Range'] = length_valid
best_match['Min_Standard_Length'] = min_length
best_match['Max_Standard_Length'] = max_length
best_match['Preferred_Standards'] = preferred_lengths[:10]
best_match['Length_mm'] = best_match['Standard_Length_mm']
else:
best_match['Standard_Length_mm'] = actual_shaft_length
best_match['Length_Deviation_mm'] = 0.0
best_match['Length_Valid'] = False
best_match['Length_In_Range'] = False
best_match['Min_Standard_Length'] = actual_shaft_length
best_match['Max_Standard_Length'] = actual_shaft_length
best_match['Preferred_Standards'] = []
best_match['Length_mm'] = actual_shaft_length
deviation = {'Head_Dia_mm': measured['Head_Dia_mm'] - best_match['Head_Dia_mm']} if best_match else {}
if best_match and 'Actual_Length_mm' in measured:
deviation['Length_mm'] = measured['Actual_Length_mm'] - best_match.get('Standard_Length_mm', 0)
status = 'pass' if best_match is not None else 'fail'
return best_match, deviation, status
# Special logic for nut: match measured AF_mm to AF_mm (Across Flats)
if ref_list and 'AF_mm' in ref_list[0] and 'AF_mm' in measured:
for ref in ref_list:
dev = abs(measured['AF_mm'] - ref['AF_mm'])
if dev < min_total_dev:
min_total_dev = dev
best_match = ref
deviation = {'AF_mm': measured['AF_mm'] - best_match['AF_mm']} if best_match else {}
status = 'pass' if best_match is not None else 'fail'
return best_match, deviation, status
# Default: Only compare numeric keys present in both measured and reference
for ref in ref_list:
total_dev = 0
compare_keys = [k for k in measured if k in ref and isinstance(measured[k], (int, float)) and isinstance(ref[k], (int, float))]
for key in compare_keys:
total_dev += abs(measured[key] - ref[key])
if total_dev < min_total_dev:
min_total_dev = total_dev
best_match = ref
deviation = {}
status = 'pass' if best_match is not None else 'fail'
if best_match is not None:
for key in measured:
if key in best_match and isinstance(measured[key], (int, float)) and isinstance(best_match[key], (int, float)):
dev = measured[key] - best_match[key]
deviation[key] = dev
else:
deviation[key] = None
return best_match, deviation, status
# ---------------- Main Spec Match ----------------
def run_spec_match(measurements_dir=None,
reference_csv_dict=None,
output_txt=None,
tolerance=0.5):
# Use default paths if not provided
if measurements_dir is None:
measurements_dir = os.path.join(PROJECT_ROOT, 'outputs/5_measured')
if output_txt is None:
output_txt = os.path.join(PROJECT_ROOT, 'outputs/6_results/spec_match_report/spec_match_results.txt')
if reference_csv_dict is None:
reference_csv_dict = {
"washer": os.path.join(PROJECT_ROOT, "data/datasets/washers_dataset.csv"),
"bolt": os.path.join(PROJECT_ROOT, "data/datasets/bolts_dataset.csv"),
"nut": os.path.join(PROJECT_ROOT, "data/datasets/nuts_dataset.csv"),
"screw": os.path.join(PROJECT_ROOT, "data/datasets/screws_dataset.csv")
}
Path(output_txt).parent.mkdir(parents=True, exist_ok=True)
# Read all reference datasets
ref_dict = {}
for obj_class, csv_path in reference_csv_dict.items():
ref_dict[obj_class] = read_reference_csv(csv_path, obj_class)
# Process all measurement txt files
measurement_files = list(Path(measurements_dir).glob('*.txt'))
results = []
for meas_file in measurement_files:
measured_data = read_measurement_txt(meas_file)
obj_class = measured_data.get('class') or measured_data.get('type')
obj_class = obj_class.lower() if obj_class else 'unknown'
if obj_class not in ref_dict:
print(f"⚠️ No reference for {obj_class} found.")
continue
best_ref, deviation, status = compare_with_reference(measured_data, ref_dict[obj_class], tolerance)
# Save results to list
results.append({
'file': str(meas_file),
'class': obj_class,
'measured': measured_data,
'reference': best_ref,
'deviation': deviation,
'status': status
})
# Write only measured and predicted (reference) values to txt
with open(output_txt, 'w') as f:
for res in results:
f.write(f"File: {res['file']}\n")
f.write(f"Class: {res['class']}\n")
f.write(f"Measured Values:\n")
for k, v in res['measured'].items():
f.write(f" {k}: {v}\n")
# Special formatting for bolts with ISO 4017 standards
if res['class'] == 'bolt' and res['reference']:
f.write(f"\nISO 4017 Standards Validation:\n")
ref = res['reference']
f.write(f" Bolt Size: {ref.get('Bolt Size', 'N/A')}\n")
f.write(f" Diameter: {ref.get('Nominal Dia', 'N/A')} mm\n")
f.write(f" Valid Range: {ref.get('Min_Standard_Length', 'N/A')} - {ref.get('Max_Standard_Length', 'N/A')} mm\n")
prefs = ref.get('Preferred_Standards', [])
if prefs:
f.write(f" Preferred Standards (first 7): {prefs}\n")
f.write(f" Snapped Length: {ref.get('Standard_Length_mm', 'N/A')} mm\n")
f.write(f" Length Deviation: {ref.get('Length_Deviation_mm', 'N/A')} mm\n")
f.write(f" In Valid Range: {'PASS' if ref.get('Length_In_Range') else 'FAIL'}\n")
if res['class'] == 'screw' and res['reference']:
f.write(f"\nScrew Standards Validation:\n")
ref = res['reference']
f.write(f" Screw Size: {ref.get('Screw Size', 'N/A')}\n")
f.write(f" Head Diameter: {ref.get('Head_Dia_mm', 'N/A')} mm\n")
f.write(f" Valid Length Range: {ref.get('Min_Standard_Length', 'N/A')} - {ref.get('Max_Standard_Length', 'N/A')} mm\n")
f.write(f" Snapped Length: {ref.get('Standard_Length_mm', 'N/A')} mm\n")
f.write(f" Length Deviation: {ref.get('Length_Deviation_mm', 'N/A')} mm\n")
f.write(f" In Valid Range: {'PASS' if ref.get('Length_In_Range') else 'FAIL'}\n")
f.write(f"\nPredicted (Reference) Values:\n")
for k, v in (res['reference'] or {}).items():
# Skip internal standards fields from output
if k not in ['Min_Standard_Length', 'Max_Standard_Length', 'Preferred_Standards',
'Standard_Length_mm', 'Length_Deviation_mm', 'Length_Valid', 'Length_In_Range']:
# For bolts, replace Length_mm with Standard_Length_mm
if res['class'] == 'bolt' and k == 'Length_mm':
standard_length = res['reference'].get('Standard_Length_mm', v)
f.write(f" {k}: {standard_length}\n")
else:
f.write(f" {k}: {v}\n")
f.write("\n")
return results
# ---------------- Test Run ----------------
# if __name__ == "__main__":
# results = run_spec_match(
# measurements_dir="Results/Measurements",
# reference_csv_dict={
# "washer": "Datasets/washers_dataset.csv",
# "bolt": "Datasets/bolts_dataset.csv",
# "nut": "Datasets/nuts_dataset.csv",
# "screw": "Datasets/screws_dataset.csv"
# },
# output_txt="Results/Predictions/spec_match_results.txt"
# )