SissiFeng commited on
Commit
477c54f
·
0 Parent(s):

Initial commit: Bambu A1 Mini analysis system

Browse files
.DS_Store ADDED
Binary file (6.15 kB). View file
 
.env.example ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MQTT Configuration
2
+ host=mqtt.bambulab.com
3
+ port=8883
4
+ username=bblp
5
+ password=bblp
6
+
7
+ # Printer Information
8
+ PRINTER_SERIAL=0309CA471800852
9
+
10
+ # S3 Configuration
11
+ AWS_ACCESS_KEY_ID=your_access_key
12
+ AWS_SECRET_ACCESS_KEY=your_secret_key
13
+ AWS_REGION=us-east-1
14
+ S3_BUCKET=bambu-prints
15
+
16
+ # Logging
17
+ LOG_LEVEL=DEBUG
18
+ LOG_FORMAT=%(asctime)s - %(levelname)s - %(message)s
.gitignore ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ env/
8
+ build/
9
+ develop-eggs/
10
+ dist/
11
+ downloads/
12
+ eggs/
13
+ .eggs/
14
+ lib/
15
+ lib64/
16
+ parts/
17
+ sdist/
18
+ var/
19
+ wheels/
20
+ *.egg-info/
21
+ .installed.cfg
22
+ *.egg
23
+
24
+ # Environment
25
+ .env
26
+ .venv
27
+ venv/
28
+ ENV/
29
+
30
+ # IDE
31
+ .idea/
32
+ .vscode/
33
+ *.swp
34
+ *.swo
35
+
36
+ # Logs
37
+ *.log
38
+
39
+ # Local files
40
+ temp/
41
+ uploads/
app.py ADDED
@@ -0,0 +1,376 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import numpy as np
3
+ import cv2
4
+ import paho.mqtt.client as mqtt
5
+ import json
6
+ import time
7
+ import threading
8
+ import os
9
+ from io import BytesIO
10
+ import zipfile
11
+ import logging
12
+ from dotenv import load_dotenv
13
+ from mo_optimizer import MOPrintOptimizer
14
+ from core.analysis import DefectDetector, ImageProcessor
15
+ from core.database import PrintJob
16
+ from datetime import datetime
17
+ import requests
18
+ from PIL import Image
19
+
20
+ # Load environment variables
21
+ load_dotenv()
22
+
23
+ # MQTT Configuration
24
+ HOST = os.getenv("host")
25
+ PORT = int(os.getenv("port", 8883))
26
+ USERNAME = os.getenv("username")
27
+ PASSWORD = os.getenv("password")
28
+ PRINTER_SERIAL = os.getenv("PRINTER_SERIAL", "0309CA471800852")
29
+
30
+ # Setup logging
31
+ logging.basicConfig(
32
+ level=logging.INFO,
33
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
34
+ )
35
+ logger = logging.getLogger(__name__)
36
+
37
+ # Global variables to store received data
38
+ latest_data = {
39
+ "bed_temperature": "N/A",
40
+ "nozzle_temperature": "N/A",
41
+ "status": "N/A",
42
+ "update_time": "Waiting for data...",
43
+ "current_image_url": None,
44
+ "print_progress": 0
45
+ }
46
+
47
+ # Initialize analysis components
48
+ optimizer = MOPrintOptimizer()
49
+ detector = DefectDetector()
50
+
51
+ def create_3mf_package(gcode_content: str, gcode_filename: str = "Metadata/plate_1.gcode") -> BytesIO:
52
+ """Create a 3MF package from G-code content"""
53
+ zip_buffer = BytesIO()
54
+ with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zipf:
55
+ zipf.writestr(gcode_filename, gcode_content)
56
+ zip_buffer.seek(0)
57
+ return zip_buffer
58
+
59
+ def create_client(host, port, username, password):
60
+ """Create and configure MQTT client"""
61
+ client = mqtt.Client()
62
+ client.username_pw_set(username, password)
63
+ client.tls_set(tls_version=mqtt.ssl.PROTOCOL_TLS)
64
+ client.on_connect = on_connect
65
+ client.on_message = on_message
66
+ try:
67
+ client.connect(host, port)
68
+ client.loop_start()
69
+ return client
70
+ except Exception as e:
71
+ logger.error(f"MQTT connection failed: {e}")
72
+ return None
73
+
74
+ def on_connect(client, userdata, flags, rc):
75
+ """MQTT connection callback"""
76
+ logger.info(f"Connected with result code {rc}")
77
+ if rc == 0:
78
+ # Subscribe to printer response topic
79
+ response_topic = f"bambu_a1_mini/response/{PRINTER_SERIAL}"
80
+ client.subscribe(response_topic)
81
+ logger.info(f"Subscribed to {response_topic}")
82
+
83
+ # Subscribe to printer status topic
84
+ status_topic = f"bambu_a1_mini/status/{PRINTER_SERIAL}"
85
+ client.subscribe(status_topic)
86
+ logger.info(f"Subscribed to {status_topic}")
87
+
88
+ # Subscribe to camera image topic
89
+ image_topic = f"bambu_a1_mini/image/{PRINTER_SERIAL}"
90
+ client.subscribe(image_topic)
91
+ logger.info(f"Subscribed to {image_topic}")
92
+
93
+ def on_message(client, userdata, message):
94
+ """MQTT message callback"""
95
+ try:
96
+ data = json.loads(message.payload)
97
+
98
+ if "image" in message.topic:
99
+ # handle new image URL
100
+ image_url = data.get("image_url")
101
+ if image_url:
102
+ latest_data.update({
103
+ "current_image_url": image_url,
104
+ "image_timestamp": data.get("timestamp"),
105
+ "square_id": data.get("square_id")
106
+ })
107
+ # update interface display
108
+ s3_image.update(value=image_url)
109
+
110
+ # record to database
111
+ if hasattr(demo, 'db_manager'):
112
+ job = demo.db_manager.session.query(PrintJob).filter_by(
113
+ square_id=data.get("square_id"),
114
+ status="printing"
115
+ ).first()
116
+ if job:
117
+ job.match_image(
118
+ image_url,
119
+ datetime.strptime(data.get("timestamp"), "%Y-%m-%d %H:%M:%S")
120
+ )
121
+ demo.db_manager.session.commit()
122
+
123
+ elif "status" in message.topic:
124
+ # update status
125
+ latest_data.update({
126
+ "status": data.get("status"),
127
+ "print_progress": data.get("progress", 0),
128
+ "current_square_id": data.get("square_id"),
129
+ "print_timestamp": data.get("timestamp"),
130
+ "update_time": time.strftime("%Y-%m-%d %H:%M:%S")
131
+ })
132
+ except Exception as e:
133
+ logger.error(f"Error processing message: {e}")
134
+
135
+ def send_print_command(params):
136
+ """Send print parameters to Pi Zero via MQTT"""
137
+ command = {
138
+ "action": "print",
139
+ "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
140
+ "square_id": params.get("square_id"), # get from position_manager
141
+ "parameters": {
142
+ "bed_temp": float(params["bed_temp"]),
143
+ "nozzle_temp": float(params["nozzle_temp"]),
144
+ "print_speed": float(params["print_speed"]),
145
+ "layer_height": float(params["layer_height"]),
146
+ "flow_rate": float(params["flow_rate"]),
147
+ "retraction_distance": float(params["retraction_distance"]),
148
+ "fan_speed": float(params["fan_speed"])
149
+ }
150
+ }
151
+
152
+ command_topic = f"bambu_a1_mini/command/{PRINTER_SERIAL}"
153
+ mqtt_client.publish(command_topic, json.dumps(command))
154
+ return "Print command sent successfully"
155
+
156
+ # Initialize MQTT client
157
+ mqtt_client = create_client(HOST, PORT, USERNAME, PASSWORD)
158
+
159
+ def update_printer_status():
160
+ """Get current printer status"""
161
+ if mqtt_client is None:
162
+ return "MQTT not connected", "No connection"
163
+
164
+ status_text = (
165
+ f"Status: {latest_data['status']}\n"
166
+ f"Progress: {latest_data['print_progress']}%\n"
167
+ f"Bed Temp: {latest_data['bed_temperature']}°C\n"
168
+ f"Nozzle Temp: {latest_data['nozzle_temperature']}°C\n"
169
+ f"Last Update: {latest_data['update_time']}"
170
+ )
171
+
172
+ job_text = (
173
+ f"Square ID: {latest_data.get('current_square_id', 'N/A')}\n"
174
+ f"Print Time: {latest_data.get('print_time', 'N/A')}\n"
175
+ f"Image Status: {'Captured' if latest_data.get('current_image_url') else 'Waiting'}"
176
+ )
177
+
178
+ return status_text, job_text
179
+
180
+ def analyze_print(image,
181
+ nozzle_temp, print_speed, layer_height,
182
+ flow_rate, retraction_distance, fan_speed):
183
+ """Analyze print quality and return evaluation results"""
184
+ if image is None:
185
+ return None, 0, 0, 0, 0, 0, 0, 0, 0
186
+
187
+ # Package current parameters
188
+ current_params = {
189
+ 'nozzle_temp': float(nozzle_temp),
190
+ 'print_speed': float(print_speed),
191
+ 'layer_height': float(layer_height),
192
+ 'flow_rate': float(flow_rate),
193
+ 'retraction_distance': float(retraction_distance),
194
+ 'fan_speed': float(fan_speed)
195
+ }
196
+
197
+ # Analyze print quality
198
+ analysis_results = detector.analyze_print(image)
199
+
200
+ # Get optimization results
201
+ results = optimizer.evaluate_objectives(image, current_params)
202
+
203
+ # Calculate quality metrics
204
+ metrics = detector.calculate_quality_metrics(image)
205
+
206
+ return (
207
+ analysis_results['binary_mask'], # Visualization
208
+ float(metrics['missing_rate']),
209
+ float(metrics['excess_rate']),
210
+ float(metrics['stringing_rate']),
211
+ float(metrics['uniformity_score']),
212
+ float(analysis_results['quality_score']),
213
+ float(results['objectives']['speed']),
214
+ float(results['objectives']['material']),
215
+ float(results['objectives']['total'])
216
+ )
217
+
218
+ # Gradio interface
219
+ with gr.Blocks(title="Bambu A1 Mini Print Analysis") as demo:
220
+ gr.Markdown("# Bambu A1 Mini Print Quality Analysis")
221
+
222
+ with gr.Row():
223
+ # print status
224
+ printer_status = gr.Textbox(
225
+ label="Printer Status",
226
+ value="Initializing...",
227
+ interactive=False
228
+ )
229
+ current_job_status = gr.Textbox(
230
+ label="Current Print Job",
231
+ value="No active print job",
232
+ interactive=False
233
+ )
234
+ refresh_btn = gr.Button("Refresh Status")
235
+
236
+ with gr.Row():
237
+ # show S3 image
238
+ s3_image = gr.Image(
239
+ label="Latest S3 Image",
240
+ type="filepath", # use URL
241
+ interactive=False
242
+ )
243
+ captured_image = gr.Image(
244
+ label="Current Print Image",
245
+ type="numpy"
246
+ )
247
+
248
+ with gr.Row():
249
+ with gr.Column():
250
+ # Print parameter inputs
251
+ nozzle_temp = gr.Slider(minimum=180, maximum=250, step=1,
252
+ value=200,
253
+ label="Nozzle Temperature (°C)")
254
+ print_speed = gr.Slider(minimum=20, maximum=150, step=1,
255
+ value=60,
256
+ label="Print Speed (mm/s)")
257
+ layer_height = gr.Slider(minimum=0.1, maximum=0.4, step=0.01,
258
+ value=0.2,
259
+ label="Layer Height (mm)")
260
+ flow_rate = gr.Slider(minimum=90, maximum=110, step=1,
261
+ value=100,
262
+ label="Flow Rate (%)")
263
+ retraction_distance = gr.Slider(minimum=0, maximum=10, step=0.1,
264
+ value=5,
265
+ label="Retraction Distance (mm)")
266
+ fan_speed = gr.Slider(minimum=0, maximum=100, step=1,
267
+ value=100,
268
+ label="Fan Speed (%)")
269
+
270
+ capture_btn = gr.Button("Capture Image")
271
+ analyze_btn = gr.Button("Analyze Print")
272
+
273
+ with gr.Column():
274
+ # Results visualization
275
+ result_image = gr.Image(label="Analysis Result")
276
+
277
+ # Quality metrics
278
+ with gr.Row():
279
+ missing_rate = gr.Number(label="Missing Rate", value=0.0)
280
+ excess_rate = gr.Number(label="Excess Rate", value=0.0)
281
+ with gr.Row():
282
+ stringing_rate = gr.Number(label="Stringing Rate", value=0.0)
283
+ uniformity_score = gr.Number(label="Uniformity Score", value=0.0)
284
+
285
+ # Overall scores
286
+ with gr.Row():
287
+ quality_output = gr.Number(label="Print Quality Score")
288
+ speed_output = gr.Number(label="Print Speed Score")
289
+ material_output = gr.Number(label="Material Efficiency Score")
290
+ total_output = gr.Number(label="Total Performance Score")
291
+
292
+ # Connect inputs to outputs
293
+ def capture_frame(img):
294
+ """Capture current image for analysis"""
295
+ if img is None:
296
+ return None
297
+ # Convert URL image to numpy array if needed
298
+ if isinstance(img, str):
299
+ response = requests.get(img)
300
+ img = Image.open(BytesIO(response.content))
301
+ img = np.array(img)
302
+ return img
303
+
304
+ capture_btn.click(
305
+ fn=capture_frame,
306
+ inputs=[s3_image],
307
+ outputs=[captured_image]
308
+ )
309
+
310
+ analyze_btn.click(
311
+ fn=lambda img, *params: (
312
+ analyze_print(img, *params),
313
+ send_print_command(dict(
314
+ nozzle_temp=params[0], # nozzle_temp slider
315
+ print_speed=params[1], # print_speed slider
316
+ layer_height=params[2], # layer_height slider
317
+ flow_rate=params[3], # flow_rate slider
318
+ retraction_distance=params[4], # retraction_distance slider
319
+ fan_speed=params[5], # fan_speed slider
320
+ bed_temp=60 # use default value, because there is no slider for bed_temp
321
+ ))
322
+ ),
323
+ inputs=[
324
+ captured_image,
325
+ nozzle_temp, print_speed, layer_height,
326
+ flow_rate, retraction_distance, fan_speed
327
+ ],
328
+ outputs=[
329
+ result_image,
330
+ missing_rate, excess_rate,
331
+ stringing_rate, uniformity_score,
332
+ quality_output, speed_output,
333
+ material_output, total_output,
334
+ printer_status, current_job_status
335
+ ]
336
+ )
337
+
338
+ # add image refresh function
339
+ def refresh_images():
340
+ """refresh image display"""
341
+ image_url = latest_data.get("current_image_url")
342
+ if image_url:
343
+ return image_url
344
+ return None
345
+
346
+ # connect refresh button
347
+ refresh_btn.click(
348
+ fn=lambda: (
349
+ update_printer_status()[0],
350
+ update_printer_status()[1],
351
+ refresh_images()
352
+ ),
353
+ outputs=[
354
+ printer_status,
355
+ current_job_status,
356
+ s3_image
357
+ ]
358
+ )
359
+
360
+ # Auto-refresh printer status
361
+ def auto_refresh():
362
+ while True:
363
+ time.sleep(5) # Update every 5 seconds
364
+ status, job_status = update_printer_status()
365
+ printer_status.update(status)
366
+ current_job_status.update(job_status)
367
+ # update image
368
+ image_url = refresh_images()
369
+ if image_url:
370
+ s3_image.update(image_url)
371
+
372
+ threading.Thread(target=auto_refresh, daemon=True).start()
373
+
374
+ # Launch the app
375
+ if __name__ == "__main__":
376
+ demo.launch()
core/.DS_Store ADDED
Binary file (6.15 kB). View file
 
core/analysis/__init__.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Bambu A1 Mini Print Analysis Core
3
+
4
+ This package provides image analysis and defect detection functionality
5
+ for the Bambu A1 Mini 3D printer.
6
+
7
+ Modules:
8
+ - image_processor: Basic image processing utilities
9
+ - defect_detector: Defect detection and analysis
10
+ - segmentation_model: Print area segmentation
11
+ """
12
+
13
+ from .image_processor import ImageProcessor
14
+ from .segmentation_model import PrintQualitySegmentation
15
+ from .defect_detector import DefectDetector
16
+
17
+ __all__ = ['ImageProcessor', 'DefectDetector', 'PrintQualitySegmentation']
core/analysis/defect_detector.py ADDED
@@ -0,0 +1,500 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import cv2
3
+ from PIL import Image
4
+ from skimage import feature, filters, morphology
5
+ from sklearn.cluster import HDBSCAN
6
+ from skimage import measure, registration
7
+ from typing import Dict, List, Tuple
8
+ from .image_processor import ImageProcessor
9
+ from .segmentation_model import PrintQualitySegmentation
10
+
11
+ class DefectDetector:
12
+ """Specialized detector for well plate / array printing quality analysis"""
13
+
14
+ def __init__(self):
15
+ self.reference_square_size = None # mm
16
+ self.pixels_per_mm = None
17
+ self.hdbscan_params = {
18
+ 'min_cluster_size': 5,
19
+ 'min_samples': 3,
20
+ 'cluster_selection_epsilon': 0.1
21
+ }
22
+
23
+ # Add groundtruth template
24
+ self.groundtruth = self._create_groundtruth_template()
25
+
26
+ self.segmentation_model = PrintQualitySegmentation()
27
+
28
+ def _create_groundtruth_template(self) -> np.ndarray:
29
+ """Create ideal print template"""
30
+ # Create a 500x500 white background
31
+ template = np.zeros((500, 500), dtype=np.uint8)
32
+
33
+ # Draw 4 perfect squares at fixed positions
34
+ square_size = 100
35
+ positions = [
36
+ (50, 50),
37
+ (350, 50),
38
+ (50, 350),
39
+ (350, 350)
40
+ ]
41
+
42
+ for x, y in positions:
43
+ cv2.rectangle(template,
44
+ (x, y),
45
+ (x + square_size, y + square_size),
46
+ 255,
47
+ -1) # -1 means fill
48
+
49
+ return template
50
+
51
+ def detect_squares(self, image):
52
+ """Detect and analyze printed squares in array
53
+
54
+ Args:
55
+ image: PIL.Image or numpy array of the print
56
+
57
+ Returns:
58
+ dict: Analysis results for each square
59
+ """
60
+ # Convert to numpy array if needed
61
+ if isinstance(image, Image.Image):
62
+ image = np.array(image)
63
+
64
+ # Convert to grayscale
65
+ gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
66
+
67
+ # Apply adaptive thresholding
68
+ thresh = cv2.adaptiveThreshold(
69
+ gray, 255,
70
+ cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
71
+ cv2.THRESH_BINARY_INV,
72
+ 21, 10
73
+ )
74
+
75
+ # Find contours
76
+ contours, _ = cv2.findContours(
77
+ thresh,
78
+ cv2.RETR_EXTERNAL,
79
+ cv2.CHAIN_APPROX_SIMPLE
80
+ )
81
+
82
+ squares = []
83
+ for contour in contours:
84
+ # Filter by area and shape
85
+ area = cv2.contourArea(contour)
86
+ if area < 100: # Minimum area threshold
87
+ continue
88
+
89
+ # Check if it's approximately square
90
+ peri = cv2.arcLength(contour, True)
91
+ approx = cv2.approxPolyDP(contour, 0.04 * peri, True)
92
+
93
+ if len(approx) == 4: # Square has 4 corners
94
+ squares.append(contour)
95
+
96
+ return self._analyze_squares(gray, squares)
97
+
98
+ def _analyze_squares(self, gray: np.ndarray, squares: List) -> Dict:
99
+ """Analyze detected squares for defects"""
100
+ results = []
101
+
102
+ for square in squares:
103
+ # Get bounding box
104
+ x, y, w, h = cv2.boundingRect(square)
105
+
106
+ # Extract square region
107
+ roi = gray[y:y+h, x:x+w]
108
+
109
+ # Analyze uniformity
110
+ uniformity = self._analyze_uniformity(roi)
111
+
112
+ # Detect gaps
113
+ gaps = self._detect_gaps(roi)
114
+
115
+ # Calculate coverage
116
+ coverage = cv2.contourArea(square) / (w * h)
117
+
118
+ results.append({
119
+ 'position': (x, y, w, h),
120
+ 'uniformity': uniformity,
121
+ 'gaps': gaps,
122
+ 'coverage': coverage
123
+ })
124
+
125
+ return {
126
+ 'square_count': len(squares),
127
+ 'squares': results
128
+ }
129
+
130
+ def _analyze_uniformity(self, roi: np.ndarray) -> float:
131
+ """Analyze print uniformity in region"""
132
+ # Calculate local standard deviation
133
+ local_std = filters.rank.std(roi, morphology.square(5))
134
+
135
+ # Normalize and invert (higher is better)
136
+ uniformity = 1 - (np.mean(local_std) / 255)
137
+ return float(uniformity)
138
+
139
+ def _detect_gaps(self, roi: np.ndarray) -> List[Dict]:
140
+ """Detect gaps in printed region"""
141
+ # Edge detection
142
+ edges = feature.canny(roi, sigma=2)
143
+
144
+ # Find connected components
145
+ labels = measure.label(edges)
146
+
147
+ # Get coordinates of edge pixels
148
+ coords = np.column_stack(np.where(edges > 0))
149
+
150
+ if len(coords) < self.hdbscan_params['min_cluster_size']:
151
+ return []
152
+
153
+ # HDBSCAN clustering
154
+ clusterer = HDBSCAN(
155
+ **self.hdbscan_params
156
+ )
157
+ cluster_labels = clusterer.fit_predict(coords)
158
+
159
+ # Analyze gap clusters
160
+ gap_clusters = []
161
+ for label in set(cluster_labels):
162
+ if label == -1:
163
+ continue
164
+
165
+ cluster_points = coords[cluster_labels == label]
166
+
167
+ # Calculate cluster properties
168
+ min_x, min_y = np.min(cluster_points, axis=0)
169
+ max_x, max_y = np.max(cluster_points, axis=0)
170
+ area = len(cluster_points)
171
+
172
+ if area > 10: # Minimum gap size
173
+ gap_clusters.append({
174
+ "location": [int(min_x), int(min_y),
175
+ int(max_x - min_x), int(max_y - min_y)],
176
+ "area": int(area)
177
+ })
178
+
179
+ return gap_clusters
180
+
181
+ def _calculate_presence_score(self, region_props: List) -> float:
182
+ """Calculate filament presence score based on region properties"""
183
+ if not region_props:
184
+ return 0.0
185
+
186
+ # Consider region sizes and shapes
187
+ scores = []
188
+ for prop in region_props:
189
+ area = prop.area
190
+ perimeter = prop.perimeter
191
+ circularity = 4 * np.pi * area / (perimeter * perimeter) if perimeter > 0 else 0
192
+
193
+ # Higher score for more circular regions
194
+ score = circularity * np.sqrt(area)
195
+ scores.append(score)
196
+
197
+ return min(1.0, np.mean(scores) / 100)
198
+
199
+ def _calculate_uniformity(self, img_gray: np.ndarray, binary: np.ndarray) -> float:
200
+ """Calculate filament uniformity score"""
201
+ # Only consider regions with filament
202
+ filament_intensities = img_gray[binary]
203
+ if len(filament_intensities) == 0:
204
+ return 0.0
205
+
206
+ # Calculate intensity statistics
207
+ std_dev = np.std(filament_intensities)
208
+ mean_intensity = np.mean(filament_intensities)
209
+
210
+ # Calculate uniformity score (inverse of coefficient of variation)
211
+ uniformity = 1.0 - (std_dev / mean_intensity if mean_intensity > 0 else 1.0)
212
+ return max(0.0, uniformity)
213
+
214
+ def calculate_quality_metrics(self, image, expected_mask=None) -> Dict:
215
+ """Calculate quantitative metrics for print quality
216
+
217
+ Args:
218
+ image: Print image
219
+ expected_mask: Expected print area mask (optional)
220
+
221
+ Returns:
222
+ dict: Dictionary containing quality metrics
223
+ """
224
+ # Convert image format
225
+ if isinstance(image, Image.Image):
226
+ image = np.array(image)
227
+ gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
228
+
229
+ # If no expected mask provided, generate using square detection
230
+ if expected_mask is None:
231
+ squares = self.detect_squares(image)
232
+ expected_mask = np.zeros_like(gray, dtype=np.uint8)
233
+ for square in squares['squares']:
234
+ x, y, w, h = square['position']
235
+ expected_mask[y:y+h, x:x+w] = 255
236
+
237
+ # 1. Calculate missing rate (Missing Rate)
238
+ _, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
239
+ missing_area = np.logical_and(binary == 0, expected_mask == 255)
240
+ missing_rate = np.sum(missing_area) / np.sum(expected_mask == 255)
241
+
242
+ # 2. Calculate excess rate (Excess Rate)
243
+ excess_area = np.logical_and(binary == 255, expected_mask == 0)
244
+ excess_rate = np.sum(excess_area) / np.sum(expected_mask == 255)
245
+
246
+ # 3. Detect stringing (Stringing)
247
+ stringing_score = self._detect_stringing_rate(gray, expected_mask)
248
+
249
+ # 4. Calculate uniformity (Uniformity)
250
+ uniformity_score = self._calculate_uniformity_score(gray, binary)
251
+
252
+ return {
253
+ "missing_rate": float(missing_rate),
254
+ "excess_rate": float(excess_rate),
255
+ "stringing_rate": float(stringing_score),
256
+ "uniformity_score": float(uniformity_score)
257
+ }
258
+
259
+ def _detect_stringing_rate(self, gray: np.ndarray, mask: np.ndarray) -> float:
260
+ """Detect stringing and calculate rate"""
261
+ # Use morphological operations to detect long structures
262
+ kernel = np.ones((3,3), np.uint8)
263
+ dilated = cv2.dilate(gray, kernel, iterations=1)
264
+ eroded = cv2.erode(gray, kernel, iterations=1)
265
+ strings = cv2.subtract(dilated, eroded)
266
+
267
+ # Only consider strings outside non-print area
268
+ strings_outside = np.logical_and(strings > 50, mask == 0)
269
+
270
+ # Calculate stringing rate
271
+ string_rate = np.sum(strings_outside) / np.sum(mask == 255)
272
+ return min(1.0, string_rate)
273
+
274
+ def _calculate_uniformity_score(self, gray: np.ndarray, binary: np.ndarray) -> float:
275
+ """Calculate uniformity score"""
276
+ # Only analyze printed area
277
+ print_area = gray[binary == 255]
278
+ if len(print_area) == 0:
279
+ return 0.0
280
+
281
+ # Calculate local standard deviation
282
+ local_std = filters.rank.std(gray, morphology.square(5))
283
+ local_std_normalized = local_std[binary == 255] / 255.0
284
+
285
+ # Convert to uniformity score (1 - mean of standard deviation)
286
+ uniformity = 1.0 - np.mean(local_std_normalized)
287
+ return max(0.0, uniformity)
288
+
289
+ def _calculate_pixel_comparison(self, image: np.ndarray, groundtruth: np.ndarray) -> Dict:
290
+ """Calculate pixel-level comparison metrics
291
+
292
+ Args:
293
+ image: Actual print image
294
+ groundtruth: Ideal template image
295
+
296
+ Returns:
297
+ dict: Pixel-level comparison metrics
298
+ """
299
+ # Ensure image size is consistent
300
+ image = cv2.resize(image, (500, 500))
301
+ if len(image.shape) == 3:
302
+ image = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
303
+
304
+ # Binarize image
305
+ _, binary = cv2.threshold(image, 127, 255, cv2.THRESH_BINARY)
306
+
307
+ # Calculate pixel matching
308
+ matching_pixels = np.sum(binary == groundtruth)
309
+ total_pixels = binary.size
310
+ pixel_accuracy = matching_pixels / total_pixels
311
+
312
+ # Calculate number of pixels for each category
313
+ true_positives = np.sum(np.logical_and(binary == 255, groundtruth == 255))
314
+ false_positives = np.sum(np.logical_and(binary == 255, groundtruth == 0))
315
+ false_negatives = np.sum(np.logical_and(binary == 0, groundtruth == 255))
316
+ true_negatives = np.sum(np.logical_and(binary == 0, groundtruth == 0))
317
+
318
+ # Calculate precision and recall
319
+ precision = true_positives / (true_positives + false_positives) if (true_positives + false_positives) > 0 else 0
320
+ recall = true_positives / (true_positives + false_negatives) if (true_positives + false_negatives) > 0 else 0
321
+
322
+ # Calculate F1 score
323
+ f1_score = 2 * (precision * recall) / (precision + recall) if (precision + recall) > 0 else 0
324
+
325
+ return {
326
+ "pixel_accuracy": float(pixel_accuracy),
327
+ "precision": float(precision),
328
+ "recall": float(recall),
329
+ "f1_score": float(f1_score),
330
+ "true_positives": int(true_positives),
331
+ "false_positives": int(false_positives),
332
+ "false_negatives": int(false_negatives),
333
+ "true_negatives": int(true_negatives)
334
+ }
335
+
336
+ def segment_print_area(self, image: np.ndarray) -> np.ndarray:
337
+ # Ensure image is grayscale
338
+ if len(image.shape) == 3:
339
+ gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
340
+ else:
341
+ gray = image
342
+
343
+ # Use adaptive thresholding for segmentation
344
+ binary = cv2.adaptiveThreshold(
345
+ gray,
346
+ 255,
347
+ cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
348
+ cv2.THRESH_BINARY_INV,
349
+ 21, # Neighborhood size
350
+ 10 # Constant difference
351
+ )
352
+
353
+ # Morphological operations to remove noise
354
+ kernel = np.ones((3,3), np.uint8)
355
+ binary = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel)
356
+ binary = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel)
357
+
358
+ return binary
359
+
360
+ def analyze_print_quality(self, image: np.ndarray) -> Tuple[np.ndarray, np.ndarray, Dict]:
361
+ """Analyze print quality and return visualization results
362
+
363
+ Args:
364
+ image: Print image
365
+
366
+ Returns:
367
+ Tuple[np.ndarray, np.ndarray, Dict]:
368
+ - groundtruth image
369
+ - defect analysis visualization image
370
+ - quality metrics dictionary
371
+ """
372
+ # Ensure image size is consistent
373
+ image = cv2.resize(image, (500, 500))
374
+
375
+ # Segment print area
376
+ binary = self.segment_print_area(image)
377
+
378
+ # Calculate quality metrics
379
+ metrics = self.calculate_quality_metrics(binary, self.groundtruth)
380
+
381
+ # Add pixel-level comparison metrics
382
+ pixel_metrics = self._calculate_pixel_comparison(binary, self.groundtruth)
383
+ metrics.update({"pixel_comparison": pixel_metrics})
384
+
385
+ # Generate defect visualization image
386
+ defect_vis = self._generate_defect_visualization(binary, self.groundtruth, metrics)
387
+
388
+ # Convert groundtruth to RGB for display
389
+ groundtruth_rgb = cv2.cvtColor(self.groundtruth, cv2.COLOR_GRAY2RGB)
390
+
391
+ return groundtruth_rgb, defect_vis, metrics
392
+
393
+ def _generate_defect_visualization(self, binary: np.ndarray,
394
+ groundtruth: np.ndarray,
395
+ metrics: Dict) -> np.ndarray:
396
+ """Generate defect visualization image"""
397
+ vis = np.zeros((500, 500, 3), dtype=np.uint8)
398
+
399
+ # Basic defect display
400
+ correct_area = np.logical_and(binary == 255, groundtruth == 255)
401
+ vis[correct_area] = [0, 255, 0] # Correct area (green)
402
+
403
+ missing_area = np.logical_and(binary == 0, groundtruth == 255)
404
+ vis[missing_area] = [255, 0, 0] # Missing area (red)
405
+
406
+ excess_area = np.logical_and(binary == 255, groundtruth == 0)
407
+ vis[excess_area] = [255, 255, 0] # Excess area (yellow)
408
+
409
+ # Use HDBSCAN to detect and label defect clusters
410
+ defect_mask = np.logical_or(missing_area, excess_area)
411
+ defect_clusters = self._detect_defect_clusters(defect_mask)
412
+
413
+ # Label defect clusters on image
414
+ for i, cluster in enumerate(defect_clusters):
415
+ # Draw bounding box
416
+ cv2.rectangle(
417
+ vis,
418
+ (int(cluster['bbox']['min_x']), int(cluster['bbox']['min_y'])),
419
+ (int(cluster['bbox']['max_x']), int(cluster['bbox']['max_y'])),
420
+ (0, 255, 255), # Cyan border
421
+ 2
422
+ )
423
+
424
+ # Label defect ID and size
425
+ label = f"D{i+1}: {cluster['size']}px"
426
+ cv2.putText(vis, label,
427
+ (int(cluster['centroid'][1]), int(cluster['centroid'][0])),
428
+ cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1)
429
+
430
+ # Add metrics text
431
+ cv2.putText(vis, f"Missing: {metrics['missing_rate']:.2f}",
432
+ (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2)
433
+ cv2.putText(vis, f"Excess: {metrics['excess_rate']:.2f}",
434
+ (10, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2)
435
+ cv2.putText(vis, f"String: {metrics['stringing_rate']:.2f}",
436
+ (10, 90), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2)
437
+ cv2.putText(vis, f"Uniformity: {metrics['uniformity_score']:.2f}",
438
+ (10, 120), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2)
439
+ cv2.putText(vis, f"Defect Clusters: {len(defect_clusters)}",
440
+ (10, 150), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2)
441
+
442
+ return vis
443
+
444
+ def _detect_defect_clusters(self, binary: np.ndarray) -> List[Dict]:
445
+ """Use HDBSCAN to detect defect clusters
446
+
447
+ Args:
448
+ binary: Binary defect image
449
+
450
+ Returns:
451
+ List[Dict]: Information for each defect area, including position and size
452
+ """
453
+ # Get defect pixel coordinates
454
+ defect_coords = np.column_stack(np.where(binary > 0))
455
+
456
+ if len(defect_coords) < self.hdbscan_params['min_cluster_size']:
457
+ return []
458
+
459
+ # Use HDBSCAN for clustering
460
+ clusterer = HDBSCAN(
461
+ min_cluster_size=self.hdbscan_params['min_cluster_size'],
462
+ min_samples=self.hdbscan_params['min_samples'],
463
+ cluster_selection_epsilon=self.hdbscan_params['cluster_selection_epsilon']
464
+ )
465
+
466
+ cluster_labels = clusterer.fit_predict(defect_coords)
467
+
468
+ # Analyze each cluster
469
+ defect_clusters = []
470
+ for label in set(cluster_labels):
471
+ if label == -1: # Noise points
472
+ continue
473
+
474
+ # Get all points in this cluster
475
+ cluster_points = defect_coords[cluster_labels == label]
476
+
477
+ # Calculate cluster statistics
478
+ centroid = np.mean(cluster_points, axis=0)
479
+ size = len(cluster_points)
480
+ bbox = {
481
+ 'min_x': np.min(cluster_points[:, 1]),
482
+ 'max_x': np.max(cluster_points[:, 1]),
483
+ 'min_y': np.min(cluster_points[:, 0]),
484
+ 'max_y': np.max(cluster_points[:, 0])
485
+ }
486
+
487
+ defect_clusters.append({
488
+ 'centroid': centroid,
489
+ 'size': size,
490
+ 'bbox': bbox,
491
+ 'points': cluster_points
492
+ })
493
+
494
+ return defect_clusters
495
+
496
+ def analyze_print(self, image):
497
+ binary_mask = self.segmentation_model.segment_print_area(image)
498
+ analysis_results = self.segmentation_model.analyze_quality(image)
499
+
500
+ return analysis_results
core/analysis/image_processor.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2
2
+ import numpy as np
3
+ from typing import Tuple, Dict, Optional
4
+ from PIL import Image
5
+
6
+ class ImageProcessor:
7
+ """Basic image processing utilities for print analysis"""
8
+
9
+ @staticmethod
10
+ def preprocess_image(image) -> np.ndarray:
11
+ """Preprocess image for analysis
12
+
13
+ Args:
14
+ image: PIL.Image or numpy array
15
+
16
+ Returns:
17
+ np.ndarray: Preprocessed image
18
+ """
19
+ # Convert to numpy array if needed
20
+ if isinstance(image, Image.Image):
21
+ image = np.array(image)
22
+
23
+ # Convert to grayscale if needed
24
+ if len(image.shape) == 3:
25
+ image = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
26
+
27
+ return image
28
+
29
+ @staticmethod
30
+ def enhance_contrast(image: np.ndarray) -> np.ndarray:
31
+ """Enhance image contrast"""
32
+ clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
33
+ return clahe.apply(image)
34
+
35
+ @staticmethod
36
+ def remove_noise(image: np.ndarray, kernel_size: int = 3) -> np.ndarray:
37
+ """Remove image noise"""
38
+ return cv2.medianBlur(image, kernel_size)
39
+
40
+ @staticmethod
41
+ def detect_edges(image: np.ndarray, sigma: float = 2.0) -> np.ndarray:
42
+ """Detect edges in image"""
43
+ return cv2.Canny(image, 100, 200)
core/analysis/segmentation_model.py ADDED
@@ -0,0 +1,1129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import cv2
3
+ from PIL import Image
4
+ from skimage import feature, filters, morphology
5
+ from datetime import datetime
6
+ from typing import Tuple, Dict
7
+
8
+ def initialize_segmentation_model():
9
+ """Initialize SegFormer model"""
10
+ model = SegformerForSemanticSegmentation.from_pretrained(
11
+ "nvidia/mit-b0",
12
+ num_labels=3, # defect, normal, background
13
+ ignore_mismatched_sizes=True
14
+ )
15
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
16
+ model.to(device)
17
+ return model, device
18
+
19
+ def process_image(image: Image.Image, model, device):
20
+ """Process image through segmentation model"""
21
+ # Transform image
22
+ transform = transforms.Compose([
23
+ transforms.Resize((512, 512)),
24
+ transforms.ToTensor(),
25
+ transforms.Normalize(mean=[0.485, 0.456, 0.406],
26
+ std=[0.229, 0.224, 0.225])
27
+ ])
28
+
29
+ input_tensor = transform(image).unsqueeze(0).to(device)
30
+
31
+ # Get predictions
32
+ with torch.no_grad():
33
+ outputs = model(input_tensor)
34
+ predictions = outputs.logits.argmax(dim=1)
35
+
36
+ return predictions.cpu().numpy()[0]
37
+
38
+ def analyze_print_quality(predictions: np.ndarray) -> dict:
39
+ """Analyze print quality from segmentation predictions"""
40
+ total_pixels = predictions.size
41
+ defect_pixels = (predictions == 0).sum().item()
42
+ normal_pixels = (predictions == 1).sum().item()
43
+
44
+ return {
45
+ 'quality_score': normal_pixels / total_pixels,
46
+ 'defect_ratio': defect_pixels / total_pixels,
47
+ 'normal_ratio': normal_pixels / total_pixels
48
+ }
49
+
50
+ class PrintQualitySegmentation:
51
+ """Independent segmentation model for print quality analysis"""
52
+
53
+ def __init__(self):
54
+ self.model_params = {
55
+ 'threshold': 127,
56
+ 'kernel_size': 3,
57
+ 'min_area': 100
58
+ }
59
+
60
+ def segment_print_area(self, image):
61
+ """Segment print area from background"""
62
+ # Convert to numpy array if needed
63
+ if isinstance(image, Image.Image):
64
+ image = np.array(image)
65
+
66
+ # Convert to grayscale if needed
67
+ if len(image.shape) == 3:
68
+ gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
69
+ else:
70
+ gray = image
71
+
72
+ # Basic thresholding
73
+ _, binary = cv2.threshold(
74
+ gray,
75
+ self.model_params['threshold'],
76
+ 255,
77
+ cv2.THRESH_BINARY
78
+ )
79
+
80
+ # Clean up noise
81
+ kernel = np.ones(
82
+ (self.model_params['kernel_size'],
83
+ self.model_params['kernel_size']),
84
+ np.uint8
85
+ )
86
+ binary = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel)
87
+
88
+ return binary
89
+
90
+ def detect_defects(self, image, binary_mask):
91
+ """Detect defects in segmented area"""
92
+ # Edge detection
93
+ edges = feature.canny(image, sigma=2)
94
+
95
+ # Find contours
96
+ contours, _ = cv2.findContours(
97
+ binary_mask,
98
+ cv2.RETR_EXTERNAL,
99
+ cv2.CHAIN_APPROX_SIMPLE
100
+ )
101
+
102
+ defects = []
103
+ for contour in contours:
104
+ area = cv2.contourArea(contour)
105
+ if area < self.model_params['min_area']:
106
+ continue
107
+
108
+ x, y, w, h = cv2.boundingRect(contour)
109
+ defects.append({
110
+ 'position': (x, y, w, h),
111
+ 'area': area
112
+ })
113
+
114
+ return defects
115
+
116
+ def analyze_quality(self, image) -> Dict:
117
+ """Analyze print quality metrics"""
118
+ binary = self.segment_print_area(image)
119
+ defects = self.detect_defects(image, binary)
120
+
121
+ total_area = np.sum(binary > 0)
122
+ defect_area = sum(d['area'] for d in defects)
123
+ quality_score = 1.0 - (defect_area / total_area if total_area > 0 else 0)
124
+
125
+ return {
126
+ 'quality_score': quality_score,
127
+ 'defect_count': len(defects),
128
+ 'total_area': total_area,
129
+ 'defect_area': defect_area,
130
+ 'binary_mask': binary,
131
+ 'defects': defects
132
+ }
133
+
134
+ def calibrate_coordinates(self, gcode_file: str, image: np.ndarray):
135
+ """Calibrate coordinate system using G-code and actual image
136
+
137
+ Args:
138
+ gcode_file: G-code file path
139
+ image: Print plate image
140
+ """
141
+ # Get printer coordinates from G-code
142
+ printer_coords = self.gcode_analyzer.get_print_coordinates(gcode_file)
143
+
144
+ # Detect actual print positions in image
145
+ detected_positions = self._detect_print_positions(image)
146
+
147
+ # Calculate coordinate mapping
148
+ if len(printer_coords) >= 2 and len(detected_positions) >= 2:
149
+ # Use at least two points to establish mapping
150
+ self.coordinate_mapping = self._calculate_mapping(
151
+ printer_coords[:2], # Printer coordinates
152
+ detected_positions[:2] # Image pixel coordinates
153
+ )
154
+
155
+ def _calculate_mapping(self, printer_coords, pixel_coords):
156
+ """Calculate coordinate mapping
157
+
158
+ Use affine transformation to calculate mapping matrix:
159
+ [x_pixel] = [a b c] [x_printer]
160
+ [y_pixel] = [d e f] [y_printer]
161
+ [1 ] [0 0 1] [1 ]
162
+ """
163
+ # Build transformation matrix
164
+ src_pts = np.float32([[x, y, 1] for x, y in printer_coords])
165
+ dst_pts = np.float32([[x, y, 1] for x, y in pixel_coords])
166
+
167
+ # Calculate transformation matrix
168
+ transform_matrix, _ = cv2.findHomography(src_pts, dst_pts)
169
+ return transform_matrix
170
+
171
+ def printer_to_pixel(self, printer_coord: tuple) -> tuple:
172
+ """Convert printer coordinates to image pixel coordinates"""
173
+ if self.coordinate_mapping is None:
174
+ raise ValueError("Coordinate mapping not calibrated")
175
+
176
+ x, y = printer_coord
177
+ point = np.array([x, y, 1])
178
+ pixel = np.dot(self.coordinate_mapping, point)
179
+ return (int(pixel[0]), int(pixel[1]))
180
+
181
+ def analyze_print_quality(self, image: np.ndarray, print_position: tuple = None) -> Tuple[np.ndarray, np.ndarray, Dict]:
182
+ """Analyze print quality, incorporating temporal information
183
+
184
+ Args:
185
+ image: Current complete print bed image
186
+ print_position: Current print position (x, y)
187
+
188
+ Returns:
189
+ groundtruth: Ideal template image
190
+ defect_vis: Defect visualization image
191
+ metrics: Quality metrics
192
+ """
193
+ # 1. Temporal segmentation: Get current print region
194
+ current_mask = self._get_current_print_mask(image, print_position)
195
+
196
+ # 2. Use SegFormer for semantic segmentation
197
+ segmentation_result = self._segment_with_model(image * current_mask)
198
+
199
+ # 3. Combine temporal information and semantic segmentation results
200
+ combined_mask = self._combine_masks(current_mask, segmentation_result)
201
+
202
+ # 4. Calculate quality metrics
203
+ metrics = self._calculate_metrics(combined_mask)
204
+
205
+ # 5. Generate visualization results
206
+ defect_vis = self._generate_visualization(image, combined_mask, metrics)
207
+
208
+ # 6. Update history
209
+ self._update_history(print_position, combined_mask)
210
+
211
+ return self.groundtruth, defect_vis, metrics
212
+
213
+ def _get_current_print_mask(self, image: np.ndarray, position: tuple) -> np.ndarray:
214
+ """Get mask of current print region"""
215
+ mask = np.zeros_like(image[:,:,0], dtype=np.uint8)
216
+
217
+ if position is not None:
218
+ # Convert printer coordinates to pixel coordinates
219
+ pixel_pos = self.printer_to_pixel(position)
220
+
221
+ # Create mask for current print region
222
+ x, y = pixel_pos
223
+ cv2.rectangle(mask,
224
+ (x, y),
225
+ (x + 100, y + 100), #
226
+ 255, -1)
227
+ else:
228
+ # First print, use position information
229
+ if position is not None:
230
+ mask = self._create_position_mask(position, image.shape[:2])
231
+
232
+ self.previous_image = image.copy()
233
+ return mask
234
+
235
+ def _create_position_mask(self, position: tuple, shape: tuple) -> np.ndarray:
236
+ """Create mask based on print position"""
237
+ mask = np.zeros(shape, dtype=np.uint8)
238
+ x, y = position
239
+ # Assume each print block size is 100x100 pixels
240
+ # TODO: Need to adjust based on actual printer parameters
241
+ cv2.rectangle(mask,
242
+ (int(x), int(y)),
243
+ (int(x + 100), int(y + 100)),
244
+ 255, -1)
245
+ return mask
246
+
247
+ def _verify_position(self, contour: np.ndarray, expected_position: tuple) -> bool:
248
+ """Verify if detected region matches expected position"""
249
+ x, y = expected_position
250
+ center = np.mean(contour.reshape(-1, 2), axis=0)
251
+ distance = np.sqrt((center[0] - x)**2 + (center[1] - y)**2)
252
+ return distance < 50 # Allow 50 pixel error
253
+
254
+ def _combine_masks(self, time_mask: np.ndarray, seg_mask: np.ndarray) -> np.ndarray:
255
+ """Combine temporal mask and semantic segmentation results"""
256
+ # Apply temporal mask to semantic segmentation result
257
+ combined = seg_mask * (time_mask > 0)
258
+ return combined
259
+
260
+ def _update_history(self, position: tuple, mask: np.ndarray):
261
+ """Update print history"""
262
+ if position is not None:
263
+ self.print_history.append({
264
+ 'position': position,
265
+ 'mask': mask.copy(),
266
+ 'timestamp': datetime.now()
267
+ })
268
+
269
+ # Extended printing parameter recommendation rules
270
+ self.parameter_rules = {
271
+ 'over_extrusion': {
272
+ 'severe': {
273
+ 'extrusion_multi': (-0.1, "Reduce extrusion multiplier by 10%"),
274
+ 'nozzle_temp': (-10, "Lower nozzle temperature by 10°C to reduce material flow"),
275
+ 'print_speed': (+5, "Slightly increase speed to reduce material per unit length"),
276
+ 'fan_speed': (+10, "Increase fan speed for better cooling"),
277
+ 'acceleration': (-500, "Reduce acceleration to minimize extrusion fluctuation"),
278
+ 'jerk': (-2, "Lower jerk to reduce extrusion instability"),
279
+ 'priority': ['extrusion_multi', 'nozzle_temp', 'fan_speed', 'print_speed', 'acceleration', 'jerk'],
280
+ 'reason': "Severe over-extrusion, comprehensive adjustment of extrusion and motion parameters"
281
+ },
282
+ 'moderate': {
283
+ 'extrusion_multi': (-0.05, "Reduce extrusion multiplier by 5%"),
284
+ 'nozzle_temp': (-5, "Slightly lower nozzle temperature by 5°C"),
285
+ 'fan_speed': (+5, "Slightly increase fan speed"),
286
+ 'priority': ['extrusion_multi', 'nozzle_temp', 'fan_speed'],
287
+ 'reason': "Minor over-extrusion, fine-tune basic parameters"
288
+ }
289
+ },
290
+ 'under_extrusion': {
291
+ 'severe': {
292
+ 'extrusion_multi': (+0.1, "Increase extrusion multiplier by 10%"),
293
+ 'nozzle_temp': (+15, "Raise nozzle temperature by 15°C for better flow"),
294
+ 'print_speed': (-10, "Reduce print speed for proper extrusion"),
295
+ 'retraction_speed': (-5, "Lower retraction speed to reduce clogging risk"),
296
+ 'retraction_distance': (-0.5, "Reduce retraction distance to prevent feed issues"),
297
+ 'min_layer_time': (+5, "Increase minimum layer time for cooling"),
298
+ 'priority': ['nozzle_temp', 'extrusion_multi', 'print_speed', 'retraction_speed', 'retraction_distance'],
299
+ 'reason': "Severe under-extrusion, comprehensive extrusion parameter adjustment needed"
300
+ },
301
+ 'moderate': {
302
+ 'extrusion_multi': (+0.05, "Increase extrusion multiplier by 5%"),
303
+ 'nozzle_temp': (+10, "Raise nozzle temperature by 10°C"),
304
+ 'priority': ['nozzle_temp', 'extrusion_multi'],
305
+ 'reason': "Minor under-extrusion, mainly adjust temperature"
306
+ }
307
+ },
308
+ 'layer_shift': {
309
+ 'severe': {
310
+ 'print_speed': (-20, "Significantly reduce print speed"),
311
+ 'acceleration': (-1000, "Significantly reduce acceleration"),
312
+ 'jerk': (-4, "Lower jerk to reduce speed"),
313
+ 'outer_wall_speed': (-10, "Lower outer wall speed for better precision"),
314
+ 'travel_speed': (-20, "Lower travel speed to reduce vibration"),
315
+ 'priority': ['acceleration', 'print_speed', 'jerk', 'outer_wall_speed', 'travel_speed'],
316
+ 'reason': "Severe layer shift, comprehensive reduction of motion parameters"
317
+ },
318
+ 'moderate': {
319
+ 'print_speed': (-10, "Slightly reduce print speed"),
320
+ 'priority': ['print_speed'],
321
+ 'reason': "Minor layer shift, reduce speed is enough"
322
+ }
323
+ },
324
+ 'stringing': {
325
+ 'severe': {
326
+ 'retraction_distance': (+1, "Increase retraction distance by 1mm"),
327
+ 'retraction_speed': (+10, "Increase retraction speed by 10mm/s"),
328
+ 'nozzle_temp': (-5, "Lower temperature to reduce oozing"),
329
+ 'travel_speed': (+20, "Increase travel speed to reduce oozing time"),
330
+ 'wipe_distance': (+2, "Increase wipe distance"),
331
+ 'coasting_volume': (+0.03, "Increase coasting volume to reduce stringing"),
332
+ 'priority': ['retraction_distance', 'retraction_speed', 'nozzle_temp', 'travel_speed', 'wipe_distance'],
333
+ 'reason': "Severe stringing, optimize retraction and motion parameters"
334
+ },
335
+ 'moderate': {
336
+ 'retraction_distance': (+0.5, "Slightly increase retraction distance by 0.5mm"),
337
+ 'nozzle_temp': (-3, "Slightly lower temperature by 3°C"),
338
+ 'priority': ['retraction_distance', 'nozzle_temp'],
339
+ 'reason': "Minor stringing, fine-tune retraction parameters"
340
+ }
341
+ },
342
+ 'warping': {
343
+ 'severe': {
344
+ 'bed_temp': (+10, "Increase bed temperature for better adhesion"),
345
+ 'first_layer_temp': (+5, "Increase first layer temperature"),
346
+ 'first_layer_speed': (-10, "Reduce first layer speed"),
347
+ 'fan_speed_initial_layer': (-50, "Reduce initial layer fan speed"),
348
+ 'skirt_distance': (-0.5, "Reduce skirt distance"),
349
+ 'brim_width': (+5, "Increase brim width"),
350
+ 'priority': ['bed_temp', 'first_layer_temp', 'fan_speed_initial_layer', 'brim_width'],
351
+ 'reason': "Severe warping, optimize first layer and temperature parameters"
352
+ }
353
+ },
354
+ 'gaps': {
355
+ 'severe': {
356
+ 'line_width': (+0.05, "Increase line width"),
357
+ 'infill_overlap': (+10, "Increase infill overlap"),
358
+ 'skin_overlap': (+25, "Increase skin overlap"),
359
+ 'infill_density': (+5, "Increase infill density"),
360
+ 'wall_thickness': (+0.4, "Increase wall thickness"),
361
+ 'priority': ['line_width', 'infill_overlap', 'skin_overlap', 'wall_thickness'],
362
+ 'reason': "Severe gaps, optimize infill and overlap parameters"
363
+ }
364
+ }
365
+ }
366
+
367
+ # Add common parameter preset combinations
368
+ self.parameter_presets = {
369
+ 'quality_first': {
370
+ 'name': "Quality-First Mode",
371
+ 'description': "Focus on print quality, suitable for small precision models",
372
+ 'params': {
373
+ 'layer_height': 0.12,
374
+ 'print_speed': 40,
375
+ 'acceleration': 500,
376
+ 'jerk': 8,
377
+ 'outer_wall_speed': 20,
378
+ 'initial_layer_speed': 15,
379
+ 'fan_speed': 100,
380
+ 'infill_density': 25,
381
+ 'wall_thickness': 1.2
382
+ },
383
+ 'suitable_for': ["Small models", "Precision parts", "Display items"],
384
+ 'trade_offs': "Longer print time but best quality"
385
+ },
386
+ 'speed_first': {
387
+ 'name': "Speed-First Mode",
388
+ 'description': "Focus on print speed, suitable for prototypes",
389
+ 'params': {
390
+ 'layer_height': 0.28,
391
+ 'print_speed': 100,
392
+ 'acceleration': 2000,
393
+ 'jerk': 15,
394
+ 'outer_wall_speed': 50,
395
+ 'initial_layer_speed': 25,
396
+ 'fan_speed': 100,
397
+ 'infill_density': 15,
398
+ 'wall_thickness': 0.8
399
+ },
400
+ 'suitable_for': ["Quick prototypes", "Concept validation", "Large simple models"],
401
+ 'trade_offs': "Rougher quality but fast printing"
402
+ },
403
+ 'balanced': {
404
+ 'name': "Balanced Mode",
405
+ 'description': "Balance between quality and speed, suitable for daily printing",
406
+ 'params': {
407
+ 'layer_height': 0.2,
408
+ 'print_speed': 60,
409
+ 'acceleration': 1000,
410
+ 'jerk': 10,
411
+ 'outer_wall_speed': 30,
412
+ 'initial_layer_speed': 20,
413
+ 'fan_speed': 100,
414
+ 'infill_density': 20,
415
+ 'wall_thickness': 1.0
416
+ },
417
+ 'suitable_for': ["Daily items", "Medium-sized models", "General parts"],
418
+ 'trade_offs': "Balanced compromise between quality and speed"
419
+ },
420
+ 'strong_mechanical': {
421
+ 'name': "Strength-First Mode",
422
+ 'description': "Focus on mechanical strength, suitable for functional parts",
423
+ 'params': {
424
+ 'layer_height': 0.2,
425
+ 'print_speed': 50,
426
+ 'wall_thickness': 1.6,
427
+ 'infill_density': 40,
428
+ 'infill_pattern': 'gyroid',
429
+ 'top_bottom_thickness': 1.2,
430
+ 'fan_speed': 80,
431
+ 'temperature': 215 # Slightly higher temperature for PLA
432
+ },
433
+ 'suitable_for': ["Mechanical parts", "Load-bearing components", "Tools"],
434
+ 'trade_offs': "Higher material usage and longer print time"
435
+ }
436
+ }
437
+
438
+ # Add specific quick solutions for problems
439
+ self.quick_fixes = {
440
+ 'stringing': {
441
+ 'name': "Quick solution for stringing",
442
+ 'description': "Quick solution for stringing problems",
443
+ 'adjustments': {
444
+ 'retraction_distance': (+1, "Increase retraction distance"),
445
+ 'retraction_speed': (+10, "Increase retraction speed"),
446
+ 'temperature': (-5, "Slightly lower temperature"),
447
+ 'travel_speed': (+20, "Increase travel speed")
448
+ },
449
+ 'explanation': "This set of parameters mainly increases retraction effect and cooling to reduce stringing"
450
+ },
451
+ 'layer_adhesion': {
452
+ 'name': "Increase layer adhesion",
453
+ 'description': "Solve layer adhesion problems",
454
+ 'adjustments': {
455
+ 'temperature': (+10, "Increase temperature"),
456
+ 'print_speed': (-5, "Slightly lower speed"),
457
+ 'fan_speed': (-20, "Lower fan speed"),
458
+ 'layer_height': (-0.04, "Reduce layer height")
459
+ },
460
+ 'explanation': "Increase temperature and reduce cooling to increase layer adhesion"
461
+ }
462
+ }
463
+
464
+ def preprocess_image(self, image):
465
+ """Preprocess image for model input
466
+
467
+ Args:
468
+ image (PIL.Image): Input image
469
+
470
+ Returns:
471
+ torch.Tensor: Preprocessed image tensor
472
+ """
473
+ # Convert to grayscale for better feature extraction
474
+ gray = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2GRAY)
475
+
476
+ # Enhance contrast
477
+ clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
478
+ enhanced = clahe.apply(gray)
479
+
480
+ # Convert back to RGB
481
+ enhanced_rgb = cv2.cvtColor(enhanced, cv2.COLOR_GRAY2RGB)
482
+ enhanced_pil = Image.fromarray(enhanced_rgb)
483
+
484
+ # Transform for model
485
+ transform = transforms.Compose([
486
+ transforms.Resize((512, 512)),
487
+ transforms.ToTensor(),
488
+ transforms.Normalize(mean=[0.485, 0.456, 0.406],
489
+ std=[0.229, 0.224, 0.225])
490
+ ])
491
+
492
+ return transform(enhanced_pil).unsqueeze(0)
493
+
494
+ def _detect_defects(self, pred_mask):
495
+ """Enhanced defect detection using HDBSCAN and traditional image processing
496
+
497
+ Args:
498
+ pred_mask (np.array): Prediction mask from segmentation model
499
+
500
+ Returns:
501
+ dict: Detected defects with their characteristics
502
+ """
503
+ defects = {}
504
+
505
+ # 1. Traditional Image Processing
506
+ # Edge detection using Canny
507
+ edges = feature.canny(
508
+ pred_mask,
509
+ sigma=2,
510
+ low_threshold=0.1,
511
+ high_threshold=0.3
512
+ )
513
+
514
+ # Local Binary Patterns for texture analysis
515
+ lbp = feature.local_binary_pattern(
516
+ pred_mask,
517
+ P=8,
518
+ R=1,
519
+ method='uniform'
520
+ )
521
+
522
+ # Blob detection for finding continuous defect regions
523
+ blobs = feature.blob_log(
524
+ pred_mask,
525
+ max_sigma=30,
526
+ num_sigma=10,
527
+ threshold=.1
528
+ )
529
+
530
+ # 2. HDBSCAN Clustering
531
+ # Convert mask to point coordinates
532
+ defect_coords = np.column_stack(np.where(pred_mask > 0))
533
+
534
+ if len(defect_coords) > 0:
535
+ # Apply HDBSCAN clustering
536
+ clusterer = HDBSCAN(
537
+ min_cluster_size=5,
538
+ min_samples=3,
539
+ metric='euclidean',
540
+ cluster_selection_epsilon=0.5
541
+ )
542
+ cluster_labels = clusterer.fit_predict(defect_coords)
543
+
544
+ # Analyze each cluster
545
+ for label in set(cluster_labels):
546
+ if label == -1: # Noise points
547
+ continue
548
+
549
+ cluster_points = defect_coords[cluster_labels == label]
550
+
551
+ # Calculate cluster characteristics
552
+ center = np.mean(cluster_points, axis=0)
553
+ size = len(cluster_points)
554
+ density = size / cv2.convexHull(cluster_points).area
555
+
556
+ # Analyze local texture using LBP
557
+ x_min, y_min = np.min(cluster_points, axis=0)
558
+ x_max, y_max = np.max(cluster_points, axis=0)
559
+ local_lbp = lbp[x_min:x_max+1, y_min:y_max+1]
560
+ texture_score = np.histogram(local_lbp, bins=10)[0]
561
+
562
+ # Calculate edge density in cluster region
563
+ local_edges = edges[x_min:x_max+1, y_min:y_max+1]
564
+ edge_density = np.sum(local_edges) / local_edges.size
565
+
566
+ # Determine defect type based on characteristics
567
+ defect_type = self._classify_defect(
568
+ density=density,
569
+ edge_density=edge_density,
570
+ texture_score=texture_score,
571
+ size=size
572
+ )
573
+
574
+ if defect_type not in defects:
575
+ defects[defect_type] = []
576
+
577
+ defects[defect_type].append({
578
+ 'center': center,
579
+ 'size': size,
580
+ 'density': density,
581
+ 'edge_density': edge_density,
582
+ 'texture_score': texture_score,
583
+ 'points': cluster_points,
584
+ 'confidence': self._calculate_confidence(
585
+ density, edge_density, size
586
+ )
587
+ })
588
+
589
+ return defects
590
+
591
+ def _classify_defect(self, density, edge_density, texture_score, size):
592
+ """Classify defect type based on characteristics
593
+
594
+ Args:
595
+ density (float): Point density in cluster
596
+ edge_density (float): Edge density in cluster region
597
+ texture_score (np.array): LBP histogram
598
+ size (int): Number of points in cluster
599
+
600
+ Returns:
601
+ str: Defect type classification
602
+ """
603
+ # High density + High edge density -> Over extrusion
604
+ if density > 0.8 and edge_density > 0.6:
605
+ return 'over_extrusion'
606
+
607
+ # Low density + High edge density -> Under extrusion
608
+ if density < 0.4 and edge_density > 0.7:
609
+ return 'under_extrusion'
610
+
611
+ # High density + Linear arrangement -> Layer shift
612
+ if density > 0.6 and self._is_linear_arrangement(texture_score):
613
+ return 'layer_shift'
614
+
615
+ # Low density + Scattered pattern -> Stringing
616
+ if density < 0.3 and self._is_scattered_pattern(texture_score):
617
+ return 'stringing'
618
+
619
+ # Default case
620
+ return 'unknown_defect'
621
+
622
+ def _calculate_confidence(self, density, edge_density, size):
623
+ """Calculate confidence score for defect detection
624
+
625
+ Args:
626
+ density (float): Point density
627
+ edge_density (float): Edge density
628
+ size (int): Cluster size
629
+
630
+ Returns:
631
+ float: Confidence score between 0 and 1
632
+ """
633
+ # Normalize size
634
+ size_score = min(size / 1000, 1.0)
635
+
636
+ # Combine metrics with weights
637
+ confidence = (
638
+ 0.4 * density +
639
+ 0.3 * edge_density +
640
+ 0.3 * size_score
641
+ )
642
+
643
+ return min(confidence, 1.0)
644
+
645
+ def _is_linear_arrangement(self, texture_score):
646
+ """Check if texture suggests linear arrangement"""
647
+ # Analyze LBP histogram for linear patterns
648
+ peak_ratio = np.max(texture_score) / np.mean(texture_score)
649
+ return peak_ratio > 3.0
650
+
651
+ def _is_scattered_pattern(self, texture_score):
652
+ """Check if texture suggests scattered pattern"""
653
+ # Analyze LBP histogram for scattered patterns
654
+ entropy = -np.sum(texture_score * np.log2(texture_score + 1e-10))
655
+ return entropy > 3.0
656
+
657
+ def detect_defects(self, image):
658
+ """Detect printing defects in real-time
659
+
660
+ Args:
661
+ image (PIL.Image): Input image from camera
662
+
663
+ Returns:
664
+ dict: {
665
+ 'mask': segmentation mask,
666
+ 'defects': list of detected defects with coordinates and types,
667
+ 'quality_score': overall quality score,
668
+ 'suggestions': parameter adjustment suggestions
669
+ }
670
+ """
671
+ # Preprocess image
672
+ img_tensor = self.preprocess_image(image)
673
+
674
+ # Get model prediction
675
+ with torch.no_grad():
676
+ outputs = self.model(img_tensor.to(self.device))
677
+ logits = outputs.logits
678
+ pred_mask = torch.argmax(logits, dim=1)[0].cpu().numpy()
679
+
680
+ # Analyze different types of defects
681
+ defects = []
682
+
683
+ # 1. Detect over-extrusion regions
684
+ over_regions = self._detect_over_extrusion(pred_mask)
685
+ defects.extend([{
686
+ 'type': 'over_extrusion',
687
+ 'region': region,
688
+ 'confidence': conf,
689
+ 'suggestion': {
690
+ 'extrusion_multi': '-0.05',
691
+ 'nozzle_temp': '-5°C',
692
+ 'reason': 'Reduce material flow and temperature'
693
+ }
694
+ } for region, conf in over_regions])
695
+
696
+ # 2. Detect under-extrusion regions
697
+ under_regions = self._detect_under_extrusion(pred_mask)
698
+ defects.extend([{
699
+ 'type': 'under_extrusion',
700
+ 'region': region,
701
+ 'confidence': conf,
702
+ 'suggestion': {
703
+ 'extrusion_multi': '+0.05',
704
+ 'nozzle_temp': '+10°C',
705
+ 'reason': 'Increase material flow and temperature'
706
+ }
707
+ } for region, conf in under_regions])
708
+
709
+ # 3. Detect layer shifts
710
+ shifts = self._detect_layer_shifts(pred_mask)
711
+ defects.extend([{
712
+ 'type': 'layer_shift',
713
+ 'region': region,
714
+ 'confidence': conf,
715
+ 'suggestion': {
716
+ 'print_speed': '-10mm/s',
717
+ 'reason': 'Reduce speed to prevent shifts'
718
+ }
719
+ } for region, conf in shifts])
720
+
721
+ # Calculate overall quality score
722
+ quality_score = self._calculate_quality_score(pred_mask, defects)
723
+
724
+ return {
725
+ 'mask': pred_mask,
726
+ 'defects': defects,
727
+ 'quality_score': quality_score,
728
+ 'visualization': self._generate_visualization(image, defects)
729
+ }
730
+
731
+ def _detect_over_extrusion(self, mask):
732
+ """Detect over-extrusion regions using image processing"""
733
+ over_mask = mask == 2 # over-extrusion class
734
+
735
+ # Find connected components
736
+ num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(
737
+ over_mask.astype(np.uint8), connectivity=8
738
+ )
739
+
740
+ regions = []
741
+ for i in range(1, num_labels): # Skip background
742
+ if stats[i, cv2.CC_STAT_AREA] > 50: # Min area threshold
743
+ x = stats[i, cv2.CC_STAT_LEFT]
744
+ y = stats[i, cv2.CC_STAT_TOP]
745
+ w = stats[i, cv2.CC_STAT_WIDTH]
746
+ h = stats[i, cv2.CC_STAT_HEIGHT]
747
+
748
+ # Calculate confidence based on area and intensity
749
+ area_score = min(stats[i, cv2.CC_STAT_AREA] / 500, 1.0)
750
+ intensity = np.mean(mask[labels == i])
751
+ conf = (area_score + intensity) / 2
752
+
753
+ regions.append(((x, y, w, h), conf))
754
+
755
+ return regions
756
+
757
+ def _detect_under_extrusion(self, mask):
758
+ """Detect under-extrusion using morphological operations"""
759
+ under_mask = mask == 3 # under-extrusion class
760
+
761
+ # Apply morphological operations to find gaps
762
+ kernel = np.ones((3,3), np.uint8)
763
+ dilated = cv2.dilate(under_mask.astype(np.uint8), kernel, iterations=1)
764
+ gaps = cv2.subtract(dilated, under_mask.astype(np.uint8))
765
+
766
+ # Find contours of gaps
767
+ contours, _ = cv2.findContours(gaps, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
768
+
769
+ regions = []
770
+ for contour in contours:
771
+ if cv2.contourArea(contour) > 30: # Min area threshold
772
+ x, y, w, h = cv2.boundingRect(contour)
773
+ # Calculate confidence based on gap size
774
+ conf = min(cv2.contourArea(contour) / 300, 1.0)
775
+ regions.append(((x, y, w, h), conf))
776
+
777
+ return regions
778
+
779
+ def _detect_layer_shifts(self, mask):
780
+ """Detect layer shifts using edge detection and line analysis"""
781
+ # Edge detection
782
+ edges = cv2.Canny(mask.astype(np.uint8), 100, 200)
783
+
784
+ # Detect lines using Hough transform
785
+ lines = cv2.HoughLinesP(edges, 1, np.pi/180, 50,
786
+ minLineLength=40, maxLineGap=10)
787
+
788
+ regions = []
789
+ if lines is not None:
790
+ for line in lines:
791
+ x1, y1, x2, y2 = line[0]
792
+ # Check for horizontal displacement
793
+ angle = np.abs(np.arctan2(y2-y1, x2-x1) * 180 / np.pi)
794
+ if 80 < angle < 100: # Near horizontal lines
795
+ conf = min((100 - np.abs(90 - angle)) / 10, 1.0)
796
+ regions.append(((x1, y1, x2-x1, 10), conf))
797
+
798
+ return regions
799
+
800
+ def _calculate_quality_score(self, mask, defects):
801
+ """Calculate overall quality score based on defects"""
802
+ # Base score from normal print area ratio
803
+ normal_ratio = np.sum(mask == 1) / mask.size
804
+ base_score = normal_ratio * 100
805
+
806
+ # Deduct points for defects based on confidence
807
+ for defect in defects:
808
+ penalty = defect['confidence'] * 10
809
+ if defect['type'] == 'layer_shift':
810
+ penalty *= 2 # Layer shifts are more serious
811
+ base_score -= penalty
812
+
813
+ return max(0, min(100, base_score))
814
+
815
+ def _analyze_pixel_defects(self, pred_mask, reference_gcode=None):
816
+ """Analyze pixel-level defects
817
+
818
+ Args:
819
+ pred_mask (np.array): Prediction mask
820
+ reference_gcode: Optional G-code reference
821
+
822
+ Returns:
823
+ dict: Detailed defect metrics
824
+ """
825
+ total_pixels = pred_mask.size
826
+ normal_print = np.sum(pred_mask == 1)
827
+ over_extrusion = np.sum(pred_mask == 2)
828
+ under_extrusion = np.sum(pred_mask == 3)
829
+
830
+ # Compare with reference if available
831
+ if reference_gcode is not None:
832
+ expected_mask = self._generate_expected_mask(reference_gcode)
833
+ unexpected_print = np.logical_and(pred_mask > 0, expected_mask == 0)
834
+ missing_print = np.logical_and(pred_mask == 0, expected_mask > 0)
835
+ else:
836
+ unexpected_print = np.zeros_like(pred_mask, dtype=bool)
837
+ missing_print = np.zeros_like(pred_mask, dtype=bool)
838
+
839
+ return {
840
+ 'normal_ratio': normal_print / total_pixels,
841
+ 'over_extrusion_ratio': over_extrusion / total_pixels,
842
+ 'under_extrusion_ratio': under_extrusion / total_pixels,
843
+ 'unexpected_print_pixels': np.sum(unexpected_print),
844
+ 'missing_print_pixels': np.sum(missing_print),
845
+ 'defect_locations': self._get_defect_locations(pred_mask)
846
+ }
847
+
848
+ def _get_defect_locations(self, pred_mask):
849
+ """Get coordinates of defect regions
850
+
851
+ Args:
852
+ pred_mask (np.array): Prediction mask
853
+
854
+ Returns:
855
+ dict: Coordinates of different defect types
856
+ """
857
+ defect_coords = {
858
+ 'over_extrusion': np.where(pred_mask == 2),
859
+ 'under_extrusion': np.where(pred_mask == 3),
860
+ }
861
+
862
+ # Group nearby defects into regions
863
+ defect_regions = {}
864
+ for defect_type, coords in defect_coords.items():
865
+ regions = self._cluster_defect_points(coords)
866
+ defect_regions[defect_type] = regions
867
+
868
+ return defect_regions
869
+
870
+ def _cluster_defect_points(self, coords):
871
+ """Cluster nearby defect points into regions using DBSCAN
872
+
873
+ Args:
874
+ coords: Tuple of x,y coordinates
875
+
876
+ Returns:
877
+ list: List of defect regions with their coordinates
878
+ """
879
+ if len(coords[0]) == 0:
880
+ return []
881
+
882
+ points = np.column_stack(coords)
883
+ clustering = DBSCAN(eps=5, min_samples=4).fit(points)
884
+
885
+ regions = []
886
+ for label in set(clustering.labels_):
887
+ if label == -1: # Noise points
888
+ continue
889
+ mask = clustering.labels_ == label
890
+ region_points = points[mask]
891
+ regions.append({
892
+ 'center': region_points.mean(axis=0),
893
+ 'size': len(region_points),
894
+ 'points': region_points
895
+ })
896
+
897
+ return regions
898
+
899
+ def _generate_expected_mask(self, reference_gcode):
900
+ # Implementation of _generate_expected_mask method
901
+ pass
902
+
903
+ def _generate_visualization(self, image, defects):
904
+ # Implementation of _generate_visualization method
905
+ pass
906
+
907
+ def generate_parameter_suggestions(self, defects):
908
+ """Generate parameter adjustment suggestions based on detected defects
909
+
910
+ Args:
911
+ defects (dict): Detected defect information
912
+
913
+ Returns:
914
+ dict: Parameter adjustment suggestions
915
+ """
916
+ suggestions = {}
917
+ current_adjustments = {} # Record already suggested adjustments
918
+
919
+ # Sort defects by confidence
920
+ sorted_defects = sorted(
921
+ defects.items(),
922
+ key=lambda x: x[1]['confidence'],
923
+ reverse=True
924
+ )
925
+
926
+ for defect_type, defect_info in sorted_defects:
927
+ if defect_info['confidence'] > 0.8:
928
+ severity = 'severe'
929
+ elif defect_info['confidence'] > 0.5:
930
+ severity = 'moderate'
931
+ else:
932
+ continue
933
+
934
+ if defect_type in self.parameter_rules:
935
+ rule = self.parameter_rules[defect_type][severity]
936
+
937
+ # Apply adjustment suggestions based on priority
938
+ for param in rule['priority']:
939
+ if param not in current_adjustments:
940
+ adjustment, reason = rule[param]
941
+ current_adjustments[param] = adjustment
942
+
943
+ if param not in suggestions:
944
+ suggestions[param] = {
945
+ 'adjustment': adjustment,
946
+ 'reason': f"{reason} (Due to {defect_type})",
947
+ 'confidence': defect_info['confidence']
948
+ }
949
+ else:
950
+ # If there's already a suggestion, take the larger adjustment
951
+ if abs(adjustment) > abs(suggestions[param]['adjustment']):
952
+ suggestions[param]['adjustment'] = adjustment
953
+ suggestions[param]['reason'] += f"\n{reason} (Due to {defect_type})"
954
+
955
+ return suggestions
956
+
957
+ def _analyze_defect_severity(self, defect_regions):
958
+ """Analyze the severity of defects
959
+
960
+ Args:
961
+ defect_regions (dict): Defect region information
962
+
963
+ Returns:
964
+ dict: Severity assessment of different defect types
965
+ """
966
+ severity = {}
967
+ for defect_type, regions in defect_regions.items():
968
+ if not regions:
969
+ continue
970
+
971
+ # Calculate total area of defect regions
972
+ total_area = sum(region['size'] for region in regions)
973
+ # Calculate maximum continuous defect region
974
+ max_area = max(region['size'] for region in regions)
975
+ # Calculate number of defect regions
976
+ num_regions = len(regions)
977
+
978
+ # Comprehensive severity assessment
979
+ severity[defect_type] = {
980
+ 'confidence': min(1.0, (total_area / 1000 + max_area / 500 + num_regions / 5) / 3),
981
+ 'total_area': total_area,
982
+ 'max_area': max_area,
983
+ 'num_regions': num_regions
984
+ }
985
+
986
+ def suggest_preset(self, model_size, quality_requirement, time_constraint):
987
+ """Recommend preset parameter combination based on printing requirements
988
+
989
+ Args:
990
+ model_size: "small"/"medium"/"large"
991
+ quality_requirement: "high"/"medium"/"low"
992
+ time_constraint: "tight"/"normal"/"relaxed"
993
+
994
+ Returns:
995
+ dict: Recommended preset configuration
996
+ """
997
+ if quality_requirement == "high" and time_constraint == "relaxed":
998
+ return self.parameter_presets['quality_first']
999
+ elif time_constraint == "tight" and quality_requirement == "low":
1000
+ return self.parameter_presets['speed_first']
1001
+ else:
1002
+ return self.parameter_presets['balanced']
1003
+
1004
+ def analyze_print_quality(self, image, gcode_layer):
1005
+ """Analyze print quality by comparing actual print with expected G-code path
1006
+
1007
+ Args:
1008
+ image (PIL.Image): Current layer image from camera
1009
+ gcode_layer (dict): Current layer G-code information
1010
+
1011
+ Returns:
1012
+ dict: Quality analysis results
1013
+ """
1014
+ # Get expected filament path from G-code
1015
+ expected_path = self.gcode_analyzer.get_layer_path(gcode_layer)
1016
+
1017
+ # Align camera image with G-code coordinates
1018
+ aligned_image = self.align_image_to_gcode(image, expected_path)
1019
+
1020
+ # Segment the actual printed material
1021
+ actual_material = self.segment_material(aligned_image)
1022
+
1023
+ # Generate expected material mask from G-code path
1024
+ expected_mask = self.generate_path_mask(expected_path)
1025
+
1026
+ # Calculate coverage metrics
1027
+ metrics = self.calculate_coverage_metrics(actual_material, expected_mask)
1028
+
1029
+ return {
1030
+ 'metrics': metrics,
1031
+ 'visualization': self.visualize_comparison(
1032
+ aligned_image, actual_material, expected_mask)
1033
+ }
1034
+
1035
+ def calculate_coverage_metrics(self, actual, expected):
1036
+ """Calculate coverage metrics between actual print and expected path
1037
+
1038
+ Args:
1039
+ actual (np.array): Binary mask of actual printed material
1040
+ expected (np.array): Binary mask of expected material from G-code
1041
+
1042
+ Returns:
1043
+ dict: Coverage metrics
1044
+ """
1045
+ # Where material should be (positive coverage)
1046
+ should_have_material = expected == 1
1047
+ correct_material = np.logical_and(actual == 1, should_have_material)
1048
+ missing_material = np.logical_and(actual == 0, should_have_material)
1049
+
1050
+ # Where material shouldn't be (negative coverage)
1051
+ should_not_have_material = expected == 0
1052
+ excess_material = np.logical_and(actual == 1, should_not_have_material)
1053
+ correct_empty = np.logical_and(actual == 0, should_not_have_material)
1054
+
1055
+ # Calculate metrics
1056
+ positive_coverage = np.sum(correct_material) / np.sum(should_have_material)
1057
+ negative_coverage = np.sum(correct_empty) / np.sum(should_not_have_material)
1058
+
1059
+ # Calculate error metrics
1060
+ missing_ratio = np.sum(missing_material) / np.sum(should_have_material)
1061
+ excess_ratio = np.sum(excess_material) / np.sum(should_not_have_material)
1062
+
1063
+ return {
1064
+ 'positive_coverage': positive_coverage, # Material where it should be
1065
+ 'negative_coverage': negative_coverage, # No material where it shouldn't be
1066
+ 'missing_ratio': missing_ratio, # Missing material ratio
1067
+ 'excess_ratio': excess_ratio, # Excess material ratio
1068
+ 'overall_score': (positive_coverage + negative_coverage) / 2
1069
+ }
1070
+
1071
+ def align_image_to_gcode(self, image, gcode_path):
1072
+ """Align camera image with G-code coordinates using reference points
1073
+
1074
+ Args:
1075
+ image (PIL.Image): Camera image
1076
+ gcode_path (dict): G-code path information
1077
+
1078
+ Returns:
1079
+ np.array: Aligned image
1080
+ """
1081
+ # Find reference points in image (e.g., bed corners, calibration marks)
1082
+ image_points = self.detect_reference_points(image)
1083
+
1084
+ # Get corresponding points from G-code coordinates
1085
+ gcode_points = self.gcode_analyzer.get_reference_points()
1086
+
1087
+ # Calculate transformation matrix
1088
+ transform_matrix = cv2.getPerspectiveTransform(
1089
+ image_points.astype(np.float32),
1090
+ gcode_points.astype(np.float32)
1091
+ )
1092
+
1093
+ # Apply transformation
1094
+ aligned_image = cv2.warpPerspective(
1095
+ np.array(image),
1096
+ transform_matrix,
1097
+ (image.width, image.height)
1098
+ )
1099
+
1100
+ return aligned_image
1101
+
1102
+ def visualize_comparison(self, image, actual, expected):
1103
+ """Generate visualization comparing actual print with expected path
1104
+
1105
+ Args:
1106
+ image (np.array): Original aligned image
1107
+ actual (np.array): Binary mask of actual material
1108
+ expected (np.array): Binary mask of expected material
1109
+
1110
+ Returns:
1111
+ PIL.Image: Visualization image
1112
+ """
1113
+ # Create RGB visualization
1114
+ vis = np.zeros((*image.shape[:2], 3), dtype=np.uint8)
1115
+
1116
+ # Green: Correct material placement
1117
+ vis[np.logical_and(actual == 1, expected == 1)] = [0, 255, 0]
1118
+
1119
+ # Red: Missing material
1120
+ vis[np.logical_and(actual == 0, expected == 1)] = [255, 0, 0]
1121
+
1122
+ # Yellow: Excess material
1123
+ vis[np.logical_and(actual == 1, expected == 0)] = [255, 255, 0]
1124
+
1125
+ # Blend with original image
1126
+ alpha = 0.6
1127
+ blended = cv2.addWeighted(image, 1-alpha, vis, alpha, 0)
1128
+
1129
+ return Image.fromarray(blended)
mo_optimizer.py ADDED
@@ -0,0 +1,232 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import hdbscan
3
+ from skimage import feature, filters
4
+ import cv2
5
+ from typing import Dict, Any
6
+
7
+ class MOPrintOptimizer:
8
+ """Multi-objective optimizer for print parameters"""
9
+
10
+ def __init__(self):
11
+ # Weights for different objectives
12
+ self.weights = {
13
+ 'quality': 0.4,
14
+ 'speed': 0.3,
15
+ 'material': 0.3
16
+ }
17
+
18
+ # Quality thresholds
19
+ self.quality_thresholds = {
20
+ 'missing_rate': 0.1, # 10% missing is bad
21
+ 'excess_rate': 0.1, # 10% excess is bad
22
+ 'stringing_rate': 0.05, # 5% stringing is bad
23
+ 'uniformity': 0.8 # At least 80% uniformity is good
24
+ }
25
+
26
+ # Material efficiency parameters
27
+ self.material_params = {
28
+ 'optimal_flow_rate': 100, # 100% flow rate
29
+ 'flow_tolerance': 10, # ±10% tolerance
30
+ 'optimal_layer_height': 0.2 # 0.2mm layer height
31
+ }
32
+
33
+ def evaluate_quality(self, metrics: Dict[str, float]) -> float:
34
+ """Evaluate print quality score
35
+
36
+ Args:
37
+ metrics: Dictionary containing quality metrics
38
+ - missing_rate: Percentage of missing material
39
+ - excess_rate: Percentage of excess material
40
+ - stringing_rate: Percentage of stringing
41
+ - uniformity_score: Score for print uniformity
42
+
43
+ Returns:
44
+ float: Quality score (0-1)
45
+ """
46
+ # Convert each metric to a score (0-1)
47
+ missing_score = 1.0 - min(1.0, metrics['missing_rate'] / self.quality_thresholds['missing_rate'])
48
+ excess_score = 1.0 - min(1.0, metrics['excess_rate'] / self.quality_thresholds['excess_rate'])
49
+ stringing_score = 1.0 - min(1.0, metrics['stringing_rate'] / self.quality_thresholds['stringing_rate'])
50
+ uniformity_score = metrics['uniformity_score']
51
+
52
+ # Combine scores with equal weights
53
+ quality_score = np.mean([
54
+ missing_score,
55
+ excess_score,
56
+ stringing_score,
57
+ uniformity_score
58
+ ])
59
+
60
+ return float(quality_score)
61
+
62
+ def evaluate_material_efficiency(self, params: Dict[str, float]) -> float:
63
+ """Evaluate material efficiency
64
+
65
+ Args:
66
+ params: Current print parameters
67
+
68
+ Returns:
69
+ float: Material efficiency score (0-1)
70
+ """
71
+ # Flow rate deviation from optimal
72
+ flow_deviation = abs(params['flow_rate'] - self.material_params['optimal_flow_rate'])
73
+ flow_score = 1.0 - min(1.0, flow_deviation / self.material_params['flow_tolerance'])
74
+
75
+ # Layer height optimization (thicker layers use less material for same volume)
76
+ layer_score = params['layer_height'] / self.material_params['optimal_layer_height']
77
+ layer_score = min(1.0, layer_score) # Cap at 1.0
78
+
79
+ # Retraction optimization (less retraction is better for material efficiency)
80
+ retraction_score = 1.0 - (params['retraction_distance'] / 10.0) # Assuming max 10mm
81
+
82
+ # Combine scores
83
+ material_score = np.mean([
84
+ flow_score * 0.4, # Flow rate is most important
85
+ layer_score * 0.4, # Layer height equally important
86
+ retraction_score * 0.2 # Retraction less important
87
+ ])
88
+
89
+ return float(material_score)
90
+
91
+ def evaluate_objectives(self, image: np.ndarray, params: Dict[str, float]) -> Dict[str, Any]:
92
+ """Evaluate all objectives and combine them
93
+
94
+ Args:
95
+ image: Print image for quality analysis
96
+ params: Current print parameters
97
+
98
+ Returns:
99
+ dict: Evaluation results including individual scores and total
100
+ """
101
+ # Get quality metrics from image analysis
102
+ quality_metrics = {
103
+ 'missing_rate': 0.05, # These should come from DefectDetector
104
+ 'excess_rate': 0.03, # in real implementation
105
+ 'stringing_rate': 0.02,
106
+ 'uniformity_score': 0.95
107
+ }
108
+
109
+ # Calculate individual objective scores
110
+ quality_score = self.evaluate_quality(quality_metrics)
111
+ speed_score = params['print_speed'] / 150.0 # Normalize to max speed
112
+ material_score = self.evaluate_material_efficiency(params)
113
+
114
+ # Combine objectives using weights
115
+ total_score = (
116
+ quality_score * self.weights['quality'] +
117
+ speed_score * self.weights['speed'] +
118
+ material_score * self.weights['material']
119
+ )
120
+
121
+ return {
122
+ 'objectives': {
123
+ 'quality': float(quality_score),
124
+ 'speed': float(speed_score),
125
+ 'material': float(material_score),
126
+ 'total': float(total_score)
127
+ },
128
+ 'metrics': quality_metrics
129
+ }
130
+
131
+ def evaluate_print_quality(self, image, expected_pattern=None):
132
+ """Evaluate print quality using hybrid approach
133
+
134
+ Args:
135
+ image: Current print image
136
+ expected_pattern: Expected print pattern (optional)
137
+
138
+ Returns:
139
+ dict: Quality metrics
140
+ """
141
+ # 1. Traditional Image Processing
142
+ edge_metrics = self._analyze_edges(image)
143
+ surface_metrics = self._analyze_surface(image)
144
+
145
+ # 2. HDBSCAN-based defect clustering
146
+ defect_metrics = self._cluster_defects(image)
147
+
148
+ # 3. Pattern matching if expected pattern provided
149
+ pattern_metrics = self._analyze_pattern(image, expected_pattern) if expected_pattern else {}
150
+
151
+ return {
152
+ 'edge_quality': edge_metrics,
153
+ 'surface_quality': surface_metrics,
154
+ 'defect_analysis': defect_metrics,
155
+ 'pattern_accuracy': pattern_metrics
156
+ }
157
+
158
+ def _analyze_edges(self, image):
159
+ """Analyze edge quality using traditional methods"""
160
+ # Multi-scale edge detection
161
+ edges_fine = feature.canny(image, sigma=1)
162
+ edges_medium = feature.canny(image, sigma=2)
163
+ edges_coarse = feature.canny(image, sigma=3)
164
+
165
+ return {
166
+ 'fine_edge_score': np.mean(edges_fine),
167
+ 'medium_edge_score': np.mean(edges_medium),
168
+ 'coarse_edge_score': np.mean(edges_coarse),
169
+ 'edge_consistency': self._calculate_edge_consistency(
170
+ [edges_fine, edges_medium, edges_coarse]
171
+ )
172
+ }
173
+
174
+ def _analyze_surface(self, image):
175
+ """Analyze surface quality using texture analysis"""
176
+ # Local Binary Patterns for texture
177
+ lbp = feature.local_binary_pattern(image, P=8, R=1, method='uniform')
178
+
179
+ # GLCM features
180
+ glcm = feature.graycomatrix(image, [1], [0, np.pi/4, np.pi/2, 3*np.pi/4])
181
+ contrast = feature.graycoprops(glcm, 'contrast')
182
+ homogeneity = feature.graycoprops(glcm, 'homogeneity')
183
+
184
+ return {
185
+ 'texture_uniformity': np.std(lbp),
186
+ 'surface_contrast': np.mean(contrast),
187
+ 'surface_homogeneity': np.mean(homogeneity)
188
+ }
189
+
190
+ def _cluster_defects(self, image):
191
+ """Use HDBSCAN to cluster potential defects"""
192
+ # Extract potential defect points
193
+ defect_points = self._extract_defect_points(image)
194
+
195
+ if len(defect_points) > 0:
196
+ # Apply HDBSCAN clustering
197
+ clusterer = hdbscan.HDBSCAN(
198
+ min_cluster_size=3,
199
+ min_samples=2,
200
+ metric='euclidean',
201
+ cluster_selection_epsilon=0.5
202
+ )
203
+ cluster_labels = clusterer.fit_predict(defect_points)
204
+
205
+ # Analyze clusters
206
+ return self._analyze_defect_clusters(defect_points, cluster_labels)
207
+
208
+ return {'defect_count': 0, 'cluster_sizes': [], 'defect_density': 0}
209
+
210
+ def _calculate_edge_consistency(self, edges):
211
+ """Calculate edge consistency"""
212
+ return np.mean([np.mean(edge) for edge in edges])
213
+
214
+ def _analyze_pattern(self, image, expected_pattern):
215
+ """Analyze pattern accuracy"""
216
+ # Placeholder for pattern matching
217
+ return 0.8 # Assuming 80% accuracy
218
+
219
+ def _extract_defect_points(self, image):
220
+ """Extract potential defect points"""
221
+ # Placeholder for defect point extraction
222
+ return np.array([[0, 0], [1, 1], [2, 2]]) # Placeholder points
223
+
224
+ def _analyze_defect_clusters(self, defect_points, cluster_labels):
225
+ """Analyze defect clusters"""
226
+ # Placeholder for cluster analysis
227
+ return {'defect_count': len(np.unique(cluster_labels)), 'cluster_sizes': [], 'defect_density': 0}
228
+
229
+ def _apply_parameter_adjustments(self, current_params, adjustments):
230
+ """Apply parameter adjustments"""
231
+ # Placeholder for parameter adjustment logic
232
+ return current_params # Placeholder return
requirements.txt ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # UI and Web
2
+ gradio>=4.0.0
3
+
4
+ # Core Data Processing
5
+ numpy>=1.20.0
6
+ pandas>=1.3.0
7
+ scipy>=1.7.0
8
+
9
+ # Image Processing
10
+ opencv-python-headless>=4.5.0
11
+ scikit-image>=0.19.0
12
+ Pillow>=8.0.0
13
+
14
+ # Machine Learning
15
+ scikit-learn>=0.24.0
16
+ hdbscan>=0.8.29
17
+
18
+ # Communication
19
+ paho-mqtt>=1.6.1
20
+
21
+ # Configuration
22
+ python-dotenv>=0.19.0
23
+
24
+ # Utilities
25
+ tqdm>=4.62.0
26
+ matplotlib>=3.4.0