three.scanner
1# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 2# This file is Auto Generated by generateScannerPy.py. Do not edit this file manually. 3# Modifications should be made to _scanner.py and then run generateScannerPy.py to update this file. 4# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 5 6## @package three 7# @file scanner.py 8# @brief Scanner class to wrap websocket connection. This file will be copied and amended with the three methods. 9# @date 2024-11-27 10# @copyright © 2024 Matter and Form. All rights reserved. 11 12from MF.V3 import Three 13from MF.V3.Task import Task 14from MF.V3.Settings.Align import Align 15from MF.V3.Settings.AutoFocus import AutoFocus 16from MF.V3.Settings.ScanSelection import ScanSelection 17from MF.V3.Settings.CaptureImage import CaptureImage 18from MF.V3.Settings.Camera import Camera 19from MF.V3.Settings.Projector import Projector 20from MF.V3.Settings.Turntable import Turntable 21from MF.V3.Settings.Capture import Capture 22from MF.V3.Settings.Scan import Scan 23from MF.V3.Settings.Export import Export 24from MF.V3.Settings.Import import Import 25from MF.V3.Settings.Merge import Merge 26from MF.V3.Settings.ScanData import ScanData 27from MF.V3.Settings.Smooth import Smooth 28from MF.V3.Settings.Advanced import Advanced 29from MF.V3.Settings.I18n import I18n 30from MF.V3.Settings.Style import Style 31from MF.V3.Settings.Tutorials import Tutorials 32from MF.V3.Settings.Viewer import Viewer 33from MF.V3.Settings.Software import Software 34 35from typing import Any, Callable, Optional, List 36import websocket 37import json 38import threading 39import time 40from MF.V3 import Task, TaskState, Buffer 41from three import __version__ 42import three.MF 43from three.serialization import TO_JSON 44from three.MF.V3.Buffer import Buffer 45 46 47 48 49class Scanner: 50 """ 51 Main class to manage and communicate with the Matter and Form THREE 3D Scanner via websocket. 52 53 Attributes: 54 * OnTask (Optional[Callable[[Task], None]]): Callback for any task related messages, default is None. Will fire for task complete, progress, and error. 55 * OnMessage (Optional[Callable[[str], None]]): Callback function to handle messages, default is None. Messages are any calls related to the system that are not tasks or buffers. Eg. {"Message":{"HasTurntable":true}} 56 * OnBuffer (Optional[Callable[[Any, bytes], None]]): Callback Function to handle buffer data, default is None. Buffers can be image or scan data. These are binary formats that are sent separately from tasks. Buffers will contain a header that describes the buffer data size. Please note that websocket buffers are limited in size and need to be sent in chunks. 57 """ 58 59 __bufferDescriptor = None 60 __buffer = None 61 __error = None 62 __taskIndex:int = 0 63 __tasks:List[Task] = [] 64 65 66 def __init__(self, 67 OnTask: Optional[Callable[[Task], None]] = None, 68 OnMessage: Optional[Callable[[str], None]] = None, 69 OnBuffer: Optional[Callable[[Any, bytes], None]] = None, 70 ): 71 """ 72 Initializes the Scanner object. 73 74 Args: 75 * OnTask (Optional[Callable[[Task], None]]): Callback for any task related messages, default is None. Will fire for task complete, progress, and error. 76 * OnMessage (Optional[Callable[[str], None]]): Callback function to handle messages, default is None. Messages are any calls related to the system that are not tasks or buffers. Eg. {"Message":{"HasTurntable":true}} 77 * OnBuffer (Optional[Callable[[Any, bytes], None]]): Callback Function to handle buffer data, default is None. Buffers can be image or scan data. These are binary formats that are sent separately from tasks. Buffers will contain a header that describes the buffer data size. Please note that websocket buffers are limited in size and need to be sent in chunks. 78 """ 79 self.__isConnected = False 80 81 self.OnTask = OnTask 82 self.OnMessage = OnMessage 83 self.OnBuffer = OnBuffer 84 85 self.__task_return_event = threading.Event() 86 87 # Dynamically add methods from Three to Scanner 88 # self._add_three_methods() 89 90 # def _add_three_methods(self): 91 # """ 92 # Dynamically adds functions from the three_methods module to the Scanner class. 93 # """ 94 # for name, func in inspect.getmembers(Three, predicate=inspect.isfunction): 95 # if not name.startswith('_'): 96 # setattr(self, name, func.__get__(self, self.__class__)) 97 98 99 def Connect(self, URI:str, timeoutSec=5) -> bool: 100 """ 101 Attempts to connect to the scanner using the specified URI and timeout. 102 103 Args: 104 * URI (str): The URI of the websocket server. 105 * timeoutSec (int): Timeout in seconds, default is 5. 106 107 Returns: 108 bool: True if connection is successful, raises Exception otherwise. 109 110 Raises: 111 Exception: If connection fails within the timeout or due to an error. 112 """ 113 print('Connecting to: ', URI) 114 self.__URI = URI 115 self.__isConnected = False 116 self.__error = None 117 118 self.__serverVersion__= None 119 120 self.websocket = websocket.WebSocketApp(self.__URI, 121 on_open=self.__OnOpen, 122 on_close=self.__OnClose, 123 on_error=self.__OnError, 124 on_message=self.__OnMessage, 125 ) 126 127 wst = threading.Thread(target=self.websocket.run_forever) 128 wst.daemon = True 129 wst.start() 130 131 # Wait for connection 132 start = time.time() 133 while time.time() < start + timeoutSec: 134 if self.__isConnected: 135 # Not checking versions => return True 136 return True 137 elif self.__error: 138 raise Exception(self.__error) 139 time.sleep(0.1) 140 141 raise Exception('Connection timeout') 142 143 def Disconnect(self) -> None: 144 """ 145 Close the websocket connection. 146 """ 147 if self.__isConnected: 148 # Close the connection 149 self.websocket.close() 150 # Wait for the connection to be closed. 151 while self.__isConnected: 152 time.sleep(0.1) 153 154 def IsConnected(self)-> bool: 155 """ 156 Checks if the scanner is currently connected. 157 158 Returns: 159 bool: True if connected, False otherwise. 160 """ 161 return self.__isConnected 162 163 def __callback(self, callback, *args) -> None: 164 if callback: 165 callback(self, *args) 166 167 # Called when the connection is opened 168 def __OnOpen(self, ws) -> None: 169 """ 170 Callback function for when the websocket connection is successfully opened. 171 172 Prints a success message to the console. 173 174 Args: 175 ws: The websocket object. 176 """ 177 self.__isConnected = True 178 print('Connected to: ', self.__URI) 179 180 # Called when the connection is closed 181 def __OnClose(self, ws, close_status_code, close_msg): 182 """ 183 Callback function for when the websocket connection is closed. 184 185 Prints a disconnect message to the console. 186 187 Args: 188 ws: The websocket object. 189 close_status_code: The code indicating why the websocket was closed. 190 close_msg: Additional message about why the websocket was closed. 191 """ 192 if self.__isConnected: 193 print('Disconnected') 194 self.__isConnected = False 195 196 # Called when an error happens 197 def __OnError(self, ws, error) -> None: 198 """ 199 Callback function for when an error occurs in the websocket connection. 200 201 Prints an error message to the console and stores the error for reference. 202 203 Args: 204 ws: The websocket object. 205 error: The error that occurred. 206 """ 207 if self.__isConnected: 208 print('Error: ', error) 209 else: 210 self.__error = error 211 212 # Called when a message arrives on the connection 213 def __OnMessage(self, ws, message) -> None: 214 """ 215 Callback function for handling messages received via the websocket. 216 217 Determines the type of message received (Task, Buffer, or general Message) and 218 triggers the corresponding handler function if one is set. 219 220 Args: 221 ws: The websocket object. 222 message: The raw message received, which can be either a byte string or a JSON string. 223 """ 224 # Bytes ? 225 if isinstance(message, bytes): 226 if self.OnBuffer: 227 228 if self.__buffer: 229 self.__buffer += message 230 else: 231 self.__buffer = message 232 if self.__bufferDescriptor.Size == len(self.__buffer): 233 self.OnBuffer(self.__bufferDescriptor, message) 234 self.__bufferDescriptor = None 235 self.__buffer = None 236 else: 237 obj = json.loads(message) 238 239 # Task 240 if 'Task' in obj: 241 # Create the task from the message 242 task = Task(**obj['Task']) 243 244 if (task.Progress): 245 # Extract the first (and only) item from the task.Progress dictionary 246 # TODO Duct tape fix due to schema weirdness for progress 247 key, process = next(iter(task.Progress.items())) 248 task.Progress = type('Progress', (object,), { 249 'current': process["current"], 250 'step': process["step"], 251 'total': process["total"] 252 })() 253 254 # Find the original task for reference 255 inputTask = self.__FindTaskWithIndex(task.Index) 256 if inputTask == None: 257 raise Exception('Task not found') 258 259 if task.Error: 260 inputTask.Error = task.Error 261 self.__OnError(self.websocket, task.Error) 262 self.__task_return_event.set() 263 264 # If assigned => Call the handler 265 if self.OnTask: 266 self.OnTask(task) 267 268 269 # If waiting for a response, set the response and notify 270 if (task.State == TaskState.Completed.value): 271 if task.Output: 272 inputTask.Output = task.Output 273 self.__task_return_event.set() 274 elif (task.State == TaskState.Failed.value): 275 inputTask.Error = task.Error 276 self.__task_return_event 277 278 # Buffer 279 elif 'Buffer' in obj: 280 self.__bufferDescriptor = Buffer(**obj['Buffer']) 281 self.__buffer = None 282 # Message 283 elif 'Message' in obj: 284 if self.OnMessage: 285 self.OnMessage(obj) 286 287 def SendTask(self, task, buffer:bytes = None) -> Any: 288 """ 289 Sends a task to the scanner. 290 Tasks are general control requests for the scanner. (eg. Camera exposure, or Get Image) 291 292 Creates a task, serializes it, and sends it via the websocket. 293 294 Args: 295 * task (Task): The task to send. 296 * buffer (bytes): The buffer data to send, default is None. 297 298 Returns: 299 Any: The task object that was sent. 300 301 Raises: 302 AssertionError: If the connection is not established. 303 """ 304 assert self.__isConnected 305 306 # Update the index 307 task.Index = self.__taskIndex 308 task.Input.Index = self.__taskIndex 309 self.__taskIndex += 1 310 311 # Send the task 312 self.__task_return_event.clear() 313 314 # Append the task 315 self.__tasks.append(task) 316 317 if buffer == None: 318 self.__SendTask(task) 319 else: 320 self.__SendTaskWithBuffer(task, buffer) 321 322 if task.Output: 323 # Wait for response 324 self.__task_return_event.wait() 325 326 self.__tasks.remove(task) 327 328 return task 329 330 # Send a task to the scanner 331 def __SendTask(self, task): 332 assert self.__isConnected 333 334 # Serialize the task 335 message = TO_JSON(task.Input) 336 337 # Build and send the message 338 message = '{"Task":' + message + '}' 339 print('Message: ', message) 340 341 self.websocket.send(message) 342 343 # Send a task with its buffer to the scanner 344 def __SendTaskWithBuffer(self, task:Task, buffer:bytes): 345 assert self.__isConnected 346 347 # Send the task 348 self.__SendTask(task) 349 350 # Build the buffer descriptor 351 bufferSize = len(buffer) 352 bufferDescriptor = Buffer(0, bufferSize, task) 353 354 # Serialize the buffer descriptor 355 bufferMessage = TO_JSON(bufferDescriptor) 356 357 # Send the buffer descriptor 358 bufferMessage = '{"Buffer":' + bufferMessage + '}' 359 self.websocket.send(bufferMessage) 360 361 # The maximum websocket payload size is 32 MB. 362 MAX_SIZE = 32000000 363 sentSize = 0 364 365 # Send all the sub-payloads of the maximum payload size. 366 while sentSize + MAX_SIZE < bufferSize: 367 self.websocket.send(buffer[sentSize:sentSize + MAX_SIZE], websocket.ABNF.OPCODE_BINARY) 368 sentSize += MAX_SIZE 369 370 # Send the remaining data. 371 if sentSize < bufferSize: 372 self.websocket.send(buffer[sentSize:bufferSize], websocket.ABNF.OPCODE_BINARY) 373 374 def __FindTaskWithIndex(self, index:int) -> Task: 375 # Find the task in the list 376 for i, t in enumerate(self.__tasks): 377 if t.Index == index: 378 return t 379 break 380 return None 381 382 # Dynamically bound functions from three.py 383 384 def add_merge_to_project(self) -> 'Task': 385 """Add a merged scan to the current project.""" 386 return Three.add_merge_to_project(self) 387 388 def align(self, source: 'int', target: 'int', rough: 'Align.Rough' = None, fine: 'Align.Fine' = None) -> 'Task': 389 """Align two scan groups.""" 390 return Three.align(self, source, target, rough, fine) 391 392 def auto_focus(self, applyAll: 'bool', cameras: 'list[AutoFocus.Camera]' = None) -> 'Task': 393 """Auto focus one or both cameras.""" 394 return Three.auto_focus(self, applyAll, cameras) 395 396 def bounding_box(self, selection: 'ScanSelection', axisAligned: 'bool') -> 'Task': 397 """Get the bounding box of a set of scan groups.""" 398 return Three.bounding_box(self, selection, axisAligned) 399 400 def calibrate_cameras(self) -> 'Task': 401 """Calibrate the cameras.""" 402 return Three.calibrate_cameras(self) 403 404 def calibrate_turntable(self) -> 'Task': 405 """Calibrate the turntable.""" 406 return Three.calibrate_turntable(self) 407 408 def calibration_capture_targets(self) -> 'Task': 409 """Get the calibration capture target for each camera calibration capture.""" 410 return Three.calibration_capture_targets(self) 411 412 def camera_calibration(self) -> 'Task': 413 """Get the camera calibration descriptor.""" 414 return Three.camera_calibration(self) 415 416 def capture_image(self, selection: 'list[int]' = None, codec: 'CaptureImage.Codec' = None, grayscale: 'bool' = None) -> 'Task': 417 """Capture a single Image.""" 418 return Three.capture_image(self, selection, codec, grayscale) 419 420 def clear_settings(self) -> 'Task': 421 """Clear scanner settings and restore the default values.""" 422 return Three.clear_settings(self) 423 424 def close_project(self) -> 'Task': 425 """Close the current open project.""" 426 return Three.close_project(self) 427 428 def connect_wifi(self, ssid: 'str', password: 'str') -> 'Task': 429 """Connect to a wifi network.""" 430 return Three.connect_wifi(self, ssid, password) 431 432 def copy_groups(self, sourceIndexes: 'list[int]' = None, targetIndex: 'int' = None, childPosition: 'int' = None, nameSuffix: 'str' = None, enumerate: 'bool' = None) -> 'Task': 433 """Copy a set of scan groups in the current open project.""" 434 return Three.copy_groups(self, sourceIndexes, targetIndex, childPosition, nameSuffix, enumerate) 435 436 def depth_map(self, camera: 'Camera' = None, projector: 'Projector' = None, turntable: 'Turntable' = None, capture: 'Capture' = None, processing: 'Scan.Processing' = None, alignWithScanner: 'bool' = None, centerAtOrigin: 'bool' = None) -> 'Task': 437 """Capture a depth map.""" 438 return Three.depth_map(self, camera, projector, turntable, capture, processing, alignWithScanner, centerAtOrigin) 439 440 def detect_calibration_card(self, Input: 'int') -> 'Task': 441 """Detect the calibration card on one or both cameras.""" 442 return Three.detect_calibration_card(self, Input) 443 444 def download_project(self, Input: 'int') -> 'Task': 445 """Download a project from the scanner.""" 446 return Three.download_project(self, Input) 447 448 def export(self, selection: 'ScanSelection' = None, texture: 'bool' = None, merge: 'bool' = None, format: 'Export.Format' = None, scale: 'float' = None, color: 'Export.Color' = None) -> 'Task': 449 """Export a group of scans.""" 450 return Three.export(self, selection, texture, merge, format, scale, color) 451 452 def export_factory_calibration_logs(self) -> 'Task': 453 """Export factory calibration logs.""" 454 return Three.export_factory_calibration_logs(self) 455 456 def export_heat_map(self, selection: 'ScanSelection' = None, texture: 'bool' = None, merge: 'bool' = None, format: 'Export.Format' = None, scale: 'float' = None, color: 'Export.Color' = None) -> 'Task': 457 """Export a mesh with vertex colors generated by the 'HeatMap' task.""" 458 return Three.export_heat_map(self, selection, texture, merge, format, scale, color) 459 460 def export_logs(self, Input: 'bool' = None) -> 'Task': 461 """Export scanner logs.""" 462 return Three.export_logs(self, Input) 463 464 def export_merge(self, selection: 'ScanSelection' = None, texture: 'bool' = None, merge: 'bool' = None, format: 'Export.Format' = None, scale: 'float' = None, color: 'Export.Color' = None) -> 'Task': 465 """Export a merged scan.""" 466 return Three.export_merge(self, selection, texture, merge, format, scale, color) 467 468 def factory_reset(self) -> 'Task': 469 """Reset the scanner to factory settings.""" 470 return Three.factory_reset(self) 471 472 def flatten_group(self, Input: 'int') -> 'Task': 473 """Flatten a scan group such that it only consists of single scans.""" 474 return Three.flatten_group(self, Input) 475 476 def forget_wifi(self) -> 'Task': 477 """Forget all wifi connections.""" 478 return Three.forget_wifi(self) 479 480 def has_cameras(self) -> 'Task': 481 """Check if the scanner has working cameras.""" 482 return Three.has_cameras(self) 483 484 def has_projector(self) -> 'Task': 485 """Check if the scanner has a working projector.""" 486 return Three.has_projector(self) 487 488 def has_turntable(self) -> 'Task': 489 """Check if the scanner is connected to a working turntable.""" 490 return Three.has_turntable(self) 491 492 def heat_map(self, sources: 'list[int]' = None, targets: 'list[int]' = None, outlierDistance: 'float' = None) -> 'Task': 493 """Compute the point-to-mesh distances of a source mesh to a target mesh and visualize as a heat map.""" 494 return Three.heat_map(self, sources, targets, outlierDistance) 495 496 def import_file(self, name: 'str' = None, scale: 'float' = None, unit: 'Import.Unit' = None, center: 'bool' = None, groupIndex: 'int' = None) -> 'Task': 497 """Import a set of 3D meshes to the current open project. The meshes must be archived in a ZIP file.""" 498 return Three.import_file(self, name, scale, unit, center, groupIndex) 499 500 def list_export_formats(self) -> 'Task': 501 """List all export formats.""" 502 return Three.list_export_formats(self) 503 504 def list_groups(self) -> 'Task': 505 """List the scan groups in the current open project.""" 506 return Three.list_groups(self) 507 508 def list_network_interfaces(self) -> 'Task': 509 """List available wifi networks.""" 510 return Three.list_network_interfaces(self) 511 512 def list_projects(self) -> 'Task': 513 """List all projects.""" 514 return Three.list_projects(self) 515 516 def list_scans(self) -> 'Task': 517 """List the scans in the current open project.""" 518 return Three.list_scans(self) 519 520 def list_settings(self) -> 'Task': 521 """Get scanner settings.""" 522 return Three.list_settings(self) 523 524 def list_wifi(self) -> 'Task': 525 """List available wifi networks.""" 526 return Three.list_wifi(self) 527 528 def merge(self, selection: 'ScanSelection' = None, remesh: 'Merge.Remesh' = None, simplify: 'Merge.Simplify' = None, texturize: 'bool' = None) -> 'Task': 529 """Merge two or more scan groups.""" 530 return Three.merge(self, selection, remesh, simplify, texturize) 531 532 def merge_data(self, index: 'int', mergeStep: 'ScanData.MergeStep' = None, buffers: 'list[ScanData.Buffer]' = None, metadata: 'list[ScanData.Metadata]' = None) -> 'Task': 533 """Download the raw scan data for the current merge process.""" 534 return Three.merge_data(self, index, mergeStep, buffers, metadata) 535 536 def move_group(self, Input: 'list[int]' = None) -> 'Task': 537 """Move a scan group.""" 538 return Three.move_group(self, Input) 539 540 def new_group(self, parentIndex: 'int' = None, baseName: 'str' = None, color: 'list[float]' = None, visible: 'bool' = None, collapsed: 'bool' = None, rotation: 'list[float]' = None, translation: 'list[float]' = None) -> 'Task': 541 """Create a new scan group.""" 542 return Three.new_group(self, parentIndex, baseName, color, visible, collapsed, rotation, translation) 543 544 def new_project(self, Input: 'str' = None) -> 'Task': 545 """Create a new project.""" 546 return Three.new_project(self, Input) 547 548 def new_scan(self, camera: 'Camera' = None, projector: 'Projector' = None, turntable: 'Turntable' = None, capture: 'Capture' = None, processing: 'Scan.Processing' = None, alignWithScanner: 'bool' = None, centerAtOrigin: 'bool' = None) -> 'Task': 549 """Capture a new scan.""" 550 return Three.new_scan(self, camera, projector, turntable, capture, processing, alignWithScanner, centerAtOrigin) 551 552 def open_project(self, Input: 'int') -> 'Task': 553 """Open an existing project.""" 554 return Three.open_project(self, Input) 555 556 def pop_settings(self, Input: 'bool' = None) -> 'Task': 557 """Pop and restore scanner settings from the settings stack.""" 558 return Three.pop_settings(self, Input) 559 560 def push_settings(self) -> 'Task': 561 """Push the current scanner settings to the settings stack.""" 562 return Three.push_settings(self) 563 564 def reboot(self) -> 'Task': 565 """Reboot the scanner.""" 566 return Three.reboot(self) 567 568 def remove_groups(self, Input: 'list[int]' = None) -> 'Task': 569 """Remove selected scan groups.""" 570 return Three.remove_groups(self, Input) 571 572 def remove_projects(self, Input: 'list[int]' = None) -> 'Task': 573 """Remove selected projects.""" 574 return Three.remove_projects(self, Input) 575 576 def restore_factory_calibration(self) -> 'Task': 577 """Restore factory calibration.""" 578 return Three.restore_factory_calibration(self) 579 580 def rotate_turntable(self, Input: 'int') -> 'Task': 581 """Rotate the turntable.""" 582 return Three.rotate_turntable(self, Input) 583 584 def scan_data(self, index: 'int', mergeStep: 'ScanData.MergeStep' = None, buffers: 'list[ScanData.Buffer]' = None, metadata: 'list[ScanData.Metadata]' = None) -> 'Task': 585 """Download the raw scan data for a scan in the current open project.""" 586 return Three.scan_data(self, index, mergeStep, buffers, metadata) 587 588 def set_cameras(self, selection: 'list[int]' = None, autoExposure: 'bool' = None, exposure: 'int' = None, analogGain: 'float' = None, digitalGain: 'int' = None, focus: 'int' = None) -> 'Task': 589 """Apply camera settings to one or both cameras.""" 590 return Three.set_cameras(self, selection, autoExposure, exposure, analogGain, digitalGain, focus) 591 592 def set_group(self, index: 'int', name: 'str' = None, color: 'list[float]' = None, visible: 'bool' = None, collapsed: 'bool' = None, rotation: 'list[float]' = None, translation: 'list[float]' = None) -> 'Task': 593 """Set scan group properties.""" 594 return Three.set_group(self, index, name, color, visible, collapsed, rotation, translation) 595 596 def set_project(self, index: 'int' = None, name: 'str' = None) -> 'Task': 597 """Apply settings to the current open project.""" 598 return Three.set_project(self, index, name) 599 600 def set_projector(self, on: 'bool' = None, brightness: 'float' = None, pattern: 'Projector.Pattern' = None, image: 'Projector.Image' = None, color: 'list[float]' = None, buffer: 'bytes' = None) -> 'Task': 601 """Apply projector settings.""" 602 return Three.set_projector(self, on, brightness, pattern, image, color, buffer) 603 604 def shutdown(self) -> 'Task': 605 """Shutdown the scanner.""" 606 return Three.shutdown(self) 607 608 def smooth(self, selection: 'ScanSelection' = None, taubin: 'Smooth.Taubin' = None) -> 'Task': 609 """Smooth a set of scans.""" 610 return Three.smooth(self, selection, taubin) 611 612 def split_group(self, Input: 'int') -> 'Task': 613 """Split a scan group (ie. move its subgroups to its parent group).""" 614 return Three.split_group(self, Input) 615 616 def start_video(self) -> 'Task': 617 """Start the video stream.""" 618 return Three.start_video(self) 619 620 def stop_video(self) -> 'Task': 621 """Stop the video stream.""" 622 return Three.stop_video(self) 623 624 def system_info(self, updateMajor: 'bool' = None, updateNightly: 'bool' = None) -> 'Task': 625 """Get system information.""" 626 return Three.system_info(self, updateMajor, updateNightly) 627 628 def transform_group(self, index: 'int', name: 'str' = None, color: 'list[float]' = None, visible: 'bool' = None, collapsed: 'bool' = None, rotation: 'list[float]' = None, translation: 'list[float]' = None) -> 'Task': 629 """Apply a rigid transformation to a group.""" 630 return Three.transform_group(self, index, name, color, visible, collapsed, rotation, translation) 631 632 def turntable_calibration(self) -> 'Task': 633 """Get the turntable calibration descriptor.""" 634 return Three.turntable_calibration(self) 635 636 def update_settings(self, advanced: 'Advanced' = None, camera: 'Camera' = None, capture: 'Capture' = None, i18n: 'I18n' = None, projector: 'Projector' = None, style: 'Style' = None, turntable: 'Turntable' = None, tutorials: 'Tutorials' = None, viewer: 'Viewer' = None, software: 'Software' = None) -> 'Task': 637 """Update scanner settings.""" 638 return Three.update_settings(self, advanced, camera, capture, i18n, projector, style, turntable, tutorials, viewer, software) 639 640 def upload_project(self, buffer: 'bytes') -> 'Task': 641 """Upload a project to the scanner.""" 642 return Three.upload_project(self, buffer)
50class Scanner: 51 """ 52 Main class to manage and communicate with the Matter and Form THREE 3D Scanner via websocket. 53 54 Attributes: 55 * OnTask (Optional[Callable[[Task], None]]): Callback for any task related messages, default is None. Will fire for task complete, progress, and error. 56 * OnMessage (Optional[Callable[[str], None]]): Callback function to handle messages, default is None. Messages are any calls related to the system that are not tasks or buffers. Eg. {"Message":{"HasTurntable":true}} 57 * OnBuffer (Optional[Callable[[Any, bytes], None]]): Callback Function to handle buffer data, default is None. Buffers can be image or scan data. These are binary formats that are sent separately from tasks. Buffers will contain a header that describes the buffer data size. Please note that websocket buffers are limited in size and need to be sent in chunks. 58 """ 59 60 __bufferDescriptor = None 61 __buffer = None 62 __error = None 63 __taskIndex:int = 0 64 __tasks:List[Task] = [] 65 66 67 def __init__(self, 68 OnTask: Optional[Callable[[Task], None]] = None, 69 OnMessage: Optional[Callable[[str], None]] = None, 70 OnBuffer: Optional[Callable[[Any, bytes], None]] = None, 71 ): 72 """ 73 Initializes the Scanner object. 74 75 Args: 76 * OnTask (Optional[Callable[[Task], None]]): Callback for any task related messages, default is None. Will fire for task complete, progress, and error. 77 * OnMessage (Optional[Callable[[str], None]]): Callback function to handle messages, default is None. Messages are any calls related to the system that are not tasks or buffers. Eg. {"Message":{"HasTurntable":true}} 78 * OnBuffer (Optional[Callable[[Any, bytes], None]]): Callback Function to handle buffer data, default is None. Buffers can be image or scan data. These are binary formats that are sent separately from tasks. Buffers will contain a header that describes the buffer data size. Please note that websocket buffers are limited in size and need to be sent in chunks. 79 """ 80 self.__isConnected = False 81 82 self.OnTask = OnTask 83 self.OnMessage = OnMessage 84 self.OnBuffer = OnBuffer 85 86 self.__task_return_event = threading.Event() 87 88 # Dynamically add methods from Three to Scanner 89 # self._add_three_methods() 90 91 # def _add_three_methods(self): 92 # """ 93 # Dynamically adds functions from the three_methods module to the Scanner class. 94 # """ 95 # for name, func in inspect.getmembers(Three, predicate=inspect.isfunction): 96 # if not name.startswith('_'): 97 # setattr(self, name, func.__get__(self, self.__class__)) 98 99 100 def Connect(self, URI:str, timeoutSec=5) -> bool: 101 """ 102 Attempts to connect to the scanner using the specified URI and timeout. 103 104 Args: 105 * URI (str): The URI of the websocket server. 106 * timeoutSec (int): Timeout in seconds, default is 5. 107 108 Returns: 109 bool: True if connection is successful, raises Exception otherwise. 110 111 Raises: 112 Exception: If connection fails within the timeout or due to an error. 113 """ 114 print('Connecting to: ', URI) 115 self.__URI = URI 116 self.__isConnected = False 117 self.__error = None 118 119 self.__serverVersion__= None 120 121 self.websocket = websocket.WebSocketApp(self.__URI, 122 on_open=self.__OnOpen, 123 on_close=self.__OnClose, 124 on_error=self.__OnError, 125 on_message=self.__OnMessage, 126 ) 127 128 wst = threading.Thread(target=self.websocket.run_forever) 129 wst.daemon = True 130 wst.start() 131 132 # Wait for connection 133 start = time.time() 134 while time.time() < start + timeoutSec: 135 if self.__isConnected: 136 # Not checking versions => return True 137 return True 138 elif self.__error: 139 raise Exception(self.__error) 140 time.sleep(0.1) 141 142 raise Exception('Connection timeout') 143 144 def Disconnect(self) -> None: 145 """ 146 Close the websocket connection. 147 """ 148 if self.__isConnected: 149 # Close the connection 150 self.websocket.close() 151 # Wait for the connection to be closed. 152 while self.__isConnected: 153 time.sleep(0.1) 154 155 def IsConnected(self)-> bool: 156 """ 157 Checks if the scanner is currently connected. 158 159 Returns: 160 bool: True if connected, False otherwise. 161 """ 162 return self.__isConnected 163 164 def __callback(self, callback, *args) -> None: 165 if callback: 166 callback(self, *args) 167 168 # Called when the connection is opened 169 def __OnOpen(self, ws) -> None: 170 """ 171 Callback function for when the websocket connection is successfully opened. 172 173 Prints a success message to the console. 174 175 Args: 176 ws: The websocket object. 177 """ 178 self.__isConnected = True 179 print('Connected to: ', self.__URI) 180 181 # Called when the connection is closed 182 def __OnClose(self, ws, close_status_code, close_msg): 183 """ 184 Callback function for when the websocket connection is closed. 185 186 Prints a disconnect message to the console. 187 188 Args: 189 ws: The websocket object. 190 close_status_code: The code indicating why the websocket was closed. 191 close_msg: Additional message about why the websocket was closed. 192 """ 193 if self.__isConnected: 194 print('Disconnected') 195 self.__isConnected = False 196 197 # Called when an error happens 198 def __OnError(self, ws, error) -> None: 199 """ 200 Callback function for when an error occurs in the websocket connection. 201 202 Prints an error message to the console and stores the error for reference. 203 204 Args: 205 ws: The websocket object. 206 error: The error that occurred. 207 """ 208 if self.__isConnected: 209 print('Error: ', error) 210 else: 211 self.__error = error 212 213 # Called when a message arrives on the connection 214 def __OnMessage(self, ws, message) -> None: 215 """ 216 Callback function for handling messages received via the websocket. 217 218 Determines the type of message received (Task, Buffer, or general Message) and 219 triggers the corresponding handler function if one is set. 220 221 Args: 222 ws: The websocket object. 223 message: The raw message received, which can be either a byte string or a JSON string. 224 """ 225 # Bytes ? 226 if isinstance(message, bytes): 227 if self.OnBuffer: 228 229 if self.__buffer: 230 self.__buffer += message 231 else: 232 self.__buffer = message 233 if self.__bufferDescriptor.Size == len(self.__buffer): 234 self.OnBuffer(self.__bufferDescriptor, message) 235 self.__bufferDescriptor = None 236 self.__buffer = None 237 else: 238 obj = json.loads(message) 239 240 # Task 241 if 'Task' in obj: 242 # Create the task from the message 243 task = Task(**obj['Task']) 244 245 if (task.Progress): 246 # Extract the first (and only) item from the task.Progress dictionary 247 # TODO Duct tape fix due to schema weirdness for progress 248 key, process = next(iter(task.Progress.items())) 249 task.Progress = type('Progress', (object,), { 250 'current': process["current"], 251 'step': process["step"], 252 'total': process["total"] 253 })() 254 255 # Find the original task for reference 256 inputTask = self.__FindTaskWithIndex(task.Index) 257 if inputTask == None: 258 raise Exception('Task not found') 259 260 if task.Error: 261 inputTask.Error = task.Error 262 self.__OnError(self.websocket, task.Error) 263 self.__task_return_event.set() 264 265 # If assigned => Call the handler 266 if self.OnTask: 267 self.OnTask(task) 268 269 270 # If waiting for a response, set the response and notify 271 if (task.State == TaskState.Completed.value): 272 if task.Output: 273 inputTask.Output = task.Output 274 self.__task_return_event.set() 275 elif (task.State == TaskState.Failed.value): 276 inputTask.Error = task.Error 277 self.__task_return_event 278 279 # Buffer 280 elif 'Buffer' in obj: 281 self.__bufferDescriptor = Buffer(**obj['Buffer']) 282 self.__buffer = None 283 # Message 284 elif 'Message' in obj: 285 if self.OnMessage: 286 self.OnMessage(obj) 287 288 def SendTask(self, task, buffer:bytes = None) -> Any: 289 """ 290 Sends a task to the scanner. 291 Tasks are general control requests for the scanner. (eg. Camera exposure, or Get Image) 292 293 Creates a task, serializes it, and sends it via the websocket. 294 295 Args: 296 * task (Task): The task to send. 297 * buffer (bytes): The buffer data to send, default is None. 298 299 Returns: 300 Any: The task object that was sent. 301 302 Raises: 303 AssertionError: If the connection is not established. 304 """ 305 assert self.__isConnected 306 307 # Update the index 308 task.Index = self.__taskIndex 309 task.Input.Index = self.__taskIndex 310 self.__taskIndex += 1 311 312 # Send the task 313 self.__task_return_event.clear() 314 315 # Append the task 316 self.__tasks.append(task) 317 318 if buffer == None: 319 self.__SendTask(task) 320 else: 321 self.__SendTaskWithBuffer(task, buffer) 322 323 if task.Output: 324 # Wait for response 325 self.__task_return_event.wait() 326 327 self.__tasks.remove(task) 328 329 return task 330 331 # Send a task to the scanner 332 def __SendTask(self, task): 333 assert self.__isConnected 334 335 # Serialize the task 336 message = TO_JSON(task.Input) 337 338 # Build and send the message 339 message = '{"Task":' + message + '}' 340 print('Message: ', message) 341 342 self.websocket.send(message) 343 344 # Send a task with its buffer to the scanner 345 def __SendTaskWithBuffer(self, task:Task, buffer:bytes): 346 assert self.__isConnected 347 348 # Send the task 349 self.__SendTask(task) 350 351 # Build the buffer descriptor 352 bufferSize = len(buffer) 353 bufferDescriptor = Buffer(0, bufferSize, task) 354 355 # Serialize the buffer descriptor 356 bufferMessage = TO_JSON(bufferDescriptor) 357 358 # Send the buffer descriptor 359 bufferMessage = '{"Buffer":' + bufferMessage + '}' 360 self.websocket.send(bufferMessage) 361 362 # The maximum websocket payload size is 32 MB. 363 MAX_SIZE = 32000000 364 sentSize = 0 365 366 # Send all the sub-payloads of the maximum payload size. 367 while sentSize + MAX_SIZE < bufferSize: 368 self.websocket.send(buffer[sentSize:sentSize + MAX_SIZE], websocket.ABNF.OPCODE_BINARY) 369 sentSize += MAX_SIZE 370 371 # Send the remaining data. 372 if sentSize < bufferSize: 373 self.websocket.send(buffer[sentSize:bufferSize], websocket.ABNF.OPCODE_BINARY) 374 375 def __FindTaskWithIndex(self, index:int) -> Task: 376 # Find the task in the list 377 for i, t in enumerate(self.__tasks): 378 if t.Index == index: 379 return t 380 break 381 return None 382 383 # Dynamically bound functions from three.py 384 385 def add_merge_to_project(self) -> 'Task': 386 """Add a merged scan to the current project.""" 387 return Three.add_merge_to_project(self) 388 389 def align(self, source: 'int', target: 'int', rough: 'Align.Rough' = None, fine: 'Align.Fine' = None) -> 'Task': 390 """Align two scan groups.""" 391 return Three.align(self, source, target, rough, fine) 392 393 def auto_focus(self, applyAll: 'bool', cameras: 'list[AutoFocus.Camera]' = None) -> 'Task': 394 """Auto focus one or both cameras.""" 395 return Three.auto_focus(self, applyAll, cameras) 396 397 def bounding_box(self, selection: 'ScanSelection', axisAligned: 'bool') -> 'Task': 398 """Get the bounding box of a set of scan groups.""" 399 return Three.bounding_box(self, selection, axisAligned) 400 401 def calibrate_cameras(self) -> 'Task': 402 """Calibrate the cameras.""" 403 return Three.calibrate_cameras(self) 404 405 def calibrate_turntable(self) -> 'Task': 406 """Calibrate the turntable.""" 407 return Three.calibrate_turntable(self) 408 409 def calibration_capture_targets(self) -> 'Task': 410 """Get the calibration capture target for each camera calibration capture.""" 411 return Three.calibration_capture_targets(self) 412 413 def camera_calibration(self) -> 'Task': 414 """Get the camera calibration descriptor.""" 415 return Three.camera_calibration(self) 416 417 def capture_image(self, selection: 'list[int]' = None, codec: 'CaptureImage.Codec' = None, grayscale: 'bool' = None) -> 'Task': 418 """Capture a single Image.""" 419 return Three.capture_image(self, selection, codec, grayscale) 420 421 def clear_settings(self) -> 'Task': 422 """Clear scanner settings and restore the default values.""" 423 return Three.clear_settings(self) 424 425 def close_project(self) -> 'Task': 426 """Close the current open project.""" 427 return Three.close_project(self) 428 429 def connect_wifi(self, ssid: 'str', password: 'str') -> 'Task': 430 """Connect to a wifi network.""" 431 return Three.connect_wifi(self, ssid, password) 432 433 def copy_groups(self, sourceIndexes: 'list[int]' = None, targetIndex: 'int' = None, childPosition: 'int' = None, nameSuffix: 'str' = None, enumerate: 'bool' = None) -> 'Task': 434 """Copy a set of scan groups in the current open project.""" 435 return Three.copy_groups(self, sourceIndexes, targetIndex, childPosition, nameSuffix, enumerate) 436 437 def depth_map(self, camera: 'Camera' = None, projector: 'Projector' = None, turntable: 'Turntable' = None, capture: 'Capture' = None, processing: 'Scan.Processing' = None, alignWithScanner: 'bool' = None, centerAtOrigin: 'bool' = None) -> 'Task': 438 """Capture a depth map.""" 439 return Three.depth_map(self, camera, projector, turntable, capture, processing, alignWithScanner, centerAtOrigin) 440 441 def detect_calibration_card(self, Input: 'int') -> 'Task': 442 """Detect the calibration card on one or both cameras.""" 443 return Three.detect_calibration_card(self, Input) 444 445 def download_project(self, Input: 'int') -> 'Task': 446 """Download a project from the scanner.""" 447 return Three.download_project(self, Input) 448 449 def export(self, selection: 'ScanSelection' = None, texture: 'bool' = None, merge: 'bool' = None, format: 'Export.Format' = None, scale: 'float' = None, color: 'Export.Color' = None) -> 'Task': 450 """Export a group of scans.""" 451 return Three.export(self, selection, texture, merge, format, scale, color) 452 453 def export_factory_calibration_logs(self) -> 'Task': 454 """Export factory calibration logs.""" 455 return Three.export_factory_calibration_logs(self) 456 457 def export_heat_map(self, selection: 'ScanSelection' = None, texture: 'bool' = None, merge: 'bool' = None, format: 'Export.Format' = None, scale: 'float' = None, color: 'Export.Color' = None) -> 'Task': 458 """Export a mesh with vertex colors generated by the 'HeatMap' task.""" 459 return Three.export_heat_map(self, selection, texture, merge, format, scale, color) 460 461 def export_logs(self, Input: 'bool' = None) -> 'Task': 462 """Export scanner logs.""" 463 return Three.export_logs(self, Input) 464 465 def export_merge(self, selection: 'ScanSelection' = None, texture: 'bool' = None, merge: 'bool' = None, format: 'Export.Format' = None, scale: 'float' = None, color: 'Export.Color' = None) -> 'Task': 466 """Export a merged scan.""" 467 return Three.export_merge(self, selection, texture, merge, format, scale, color) 468 469 def factory_reset(self) -> 'Task': 470 """Reset the scanner to factory settings.""" 471 return Three.factory_reset(self) 472 473 def flatten_group(self, Input: 'int') -> 'Task': 474 """Flatten a scan group such that it only consists of single scans.""" 475 return Three.flatten_group(self, Input) 476 477 def forget_wifi(self) -> 'Task': 478 """Forget all wifi connections.""" 479 return Three.forget_wifi(self) 480 481 def has_cameras(self) -> 'Task': 482 """Check if the scanner has working cameras.""" 483 return Three.has_cameras(self) 484 485 def has_projector(self) -> 'Task': 486 """Check if the scanner has a working projector.""" 487 return Three.has_projector(self) 488 489 def has_turntable(self) -> 'Task': 490 """Check if the scanner is connected to a working turntable.""" 491 return Three.has_turntable(self) 492 493 def heat_map(self, sources: 'list[int]' = None, targets: 'list[int]' = None, outlierDistance: 'float' = None) -> 'Task': 494 """Compute the point-to-mesh distances of a source mesh to a target mesh and visualize as a heat map.""" 495 return Three.heat_map(self, sources, targets, outlierDistance) 496 497 def import_file(self, name: 'str' = None, scale: 'float' = None, unit: 'Import.Unit' = None, center: 'bool' = None, groupIndex: 'int' = None) -> 'Task': 498 """Import a set of 3D meshes to the current open project. The meshes must be archived in a ZIP file.""" 499 return Three.import_file(self, name, scale, unit, center, groupIndex) 500 501 def list_export_formats(self) -> 'Task': 502 """List all export formats.""" 503 return Three.list_export_formats(self) 504 505 def list_groups(self) -> 'Task': 506 """List the scan groups in the current open project.""" 507 return Three.list_groups(self) 508 509 def list_network_interfaces(self) -> 'Task': 510 """List available wifi networks.""" 511 return Three.list_network_interfaces(self) 512 513 def list_projects(self) -> 'Task': 514 """List all projects.""" 515 return Three.list_projects(self) 516 517 def list_scans(self) -> 'Task': 518 """List the scans in the current open project.""" 519 return Three.list_scans(self) 520 521 def list_settings(self) -> 'Task': 522 """Get scanner settings.""" 523 return Three.list_settings(self) 524 525 def list_wifi(self) -> 'Task': 526 """List available wifi networks.""" 527 return Three.list_wifi(self) 528 529 def merge(self, selection: 'ScanSelection' = None, remesh: 'Merge.Remesh' = None, simplify: 'Merge.Simplify' = None, texturize: 'bool' = None) -> 'Task': 530 """Merge two or more scan groups.""" 531 return Three.merge(self, selection, remesh, simplify, texturize) 532 533 def merge_data(self, index: 'int', mergeStep: 'ScanData.MergeStep' = None, buffers: 'list[ScanData.Buffer]' = None, metadata: 'list[ScanData.Metadata]' = None) -> 'Task': 534 """Download the raw scan data for the current merge process.""" 535 return Three.merge_data(self, index, mergeStep, buffers, metadata) 536 537 def move_group(self, Input: 'list[int]' = None) -> 'Task': 538 """Move a scan group.""" 539 return Three.move_group(self, Input) 540 541 def new_group(self, parentIndex: 'int' = None, baseName: 'str' = None, color: 'list[float]' = None, visible: 'bool' = None, collapsed: 'bool' = None, rotation: 'list[float]' = None, translation: 'list[float]' = None) -> 'Task': 542 """Create a new scan group.""" 543 return Three.new_group(self, parentIndex, baseName, color, visible, collapsed, rotation, translation) 544 545 def new_project(self, Input: 'str' = None) -> 'Task': 546 """Create a new project.""" 547 return Three.new_project(self, Input) 548 549 def new_scan(self, camera: 'Camera' = None, projector: 'Projector' = None, turntable: 'Turntable' = None, capture: 'Capture' = None, processing: 'Scan.Processing' = None, alignWithScanner: 'bool' = None, centerAtOrigin: 'bool' = None) -> 'Task': 550 """Capture a new scan.""" 551 return Three.new_scan(self, camera, projector, turntable, capture, processing, alignWithScanner, centerAtOrigin) 552 553 def open_project(self, Input: 'int') -> 'Task': 554 """Open an existing project.""" 555 return Three.open_project(self, Input) 556 557 def pop_settings(self, Input: 'bool' = None) -> 'Task': 558 """Pop and restore scanner settings from the settings stack.""" 559 return Three.pop_settings(self, Input) 560 561 def push_settings(self) -> 'Task': 562 """Push the current scanner settings to the settings stack.""" 563 return Three.push_settings(self) 564 565 def reboot(self) -> 'Task': 566 """Reboot the scanner.""" 567 return Three.reboot(self) 568 569 def remove_groups(self, Input: 'list[int]' = None) -> 'Task': 570 """Remove selected scan groups.""" 571 return Three.remove_groups(self, Input) 572 573 def remove_projects(self, Input: 'list[int]' = None) -> 'Task': 574 """Remove selected projects.""" 575 return Three.remove_projects(self, Input) 576 577 def restore_factory_calibration(self) -> 'Task': 578 """Restore factory calibration.""" 579 return Three.restore_factory_calibration(self) 580 581 def rotate_turntable(self, Input: 'int') -> 'Task': 582 """Rotate the turntable.""" 583 return Three.rotate_turntable(self, Input) 584 585 def scan_data(self, index: 'int', mergeStep: 'ScanData.MergeStep' = None, buffers: 'list[ScanData.Buffer]' = None, metadata: 'list[ScanData.Metadata]' = None) -> 'Task': 586 """Download the raw scan data for a scan in the current open project.""" 587 return Three.scan_data(self, index, mergeStep, buffers, metadata) 588 589 def set_cameras(self, selection: 'list[int]' = None, autoExposure: 'bool' = None, exposure: 'int' = None, analogGain: 'float' = None, digitalGain: 'int' = None, focus: 'int' = None) -> 'Task': 590 """Apply camera settings to one or both cameras.""" 591 return Three.set_cameras(self, selection, autoExposure, exposure, analogGain, digitalGain, focus) 592 593 def set_group(self, index: 'int', name: 'str' = None, color: 'list[float]' = None, visible: 'bool' = None, collapsed: 'bool' = None, rotation: 'list[float]' = None, translation: 'list[float]' = None) -> 'Task': 594 """Set scan group properties.""" 595 return Three.set_group(self, index, name, color, visible, collapsed, rotation, translation) 596 597 def set_project(self, index: 'int' = None, name: 'str' = None) -> 'Task': 598 """Apply settings to the current open project.""" 599 return Three.set_project(self, index, name) 600 601 def set_projector(self, on: 'bool' = None, brightness: 'float' = None, pattern: 'Projector.Pattern' = None, image: 'Projector.Image' = None, color: 'list[float]' = None, buffer: 'bytes' = None) -> 'Task': 602 """Apply projector settings.""" 603 return Three.set_projector(self, on, brightness, pattern, image, color, buffer) 604 605 def shutdown(self) -> 'Task': 606 """Shutdown the scanner.""" 607 return Three.shutdown(self) 608 609 def smooth(self, selection: 'ScanSelection' = None, taubin: 'Smooth.Taubin' = None) -> 'Task': 610 """Smooth a set of scans.""" 611 return Three.smooth(self, selection, taubin) 612 613 def split_group(self, Input: 'int') -> 'Task': 614 """Split a scan group (ie. move its subgroups to its parent group).""" 615 return Three.split_group(self, Input) 616 617 def start_video(self) -> 'Task': 618 """Start the video stream.""" 619 return Three.start_video(self) 620 621 def stop_video(self) -> 'Task': 622 """Stop the video stream.""" 623 return Three.stop_video(self) 624 625 def system_info(self, updateMajor: 'bool' = None, updateNightly: 'bool' = None) -> 'Task': 626 """Get system information.""" 627 return Three.system_info(self, updateMajor, updateNightly) 628 629 def transform_group(self, index: 'int', name: 'str' = None, color: 'list[float]' = None, visible: 'bool' = None, collapsed: 'bool' = None, rotation: 'list[float]' = None, translation: 'list[float]' = None) -> 'Task': 630 """Apply a rigid transformation to a group.""" 631 return Three.transform_group(self, index, name, color, visible, collapsed, rotation, translation) 632 633 def turntable_calibration(self) -> 'Task': 634 """Get the turntable calibration descriptor.""" 635 return Three.turntable_calibration(self) 636 637 def update_settings(self, advanced: 'Advanced' = None, camera: 'Camera' = None, capture: 'Capture' = None, i18n: 'I18n' = None, projector: 'Projector' = None, style: 'Style' = None, turntable: 'Turntable' = None, tutorials: 'Tutorials' = None, viewer: 'Viewer' = None, software: 'Software' = None) -> 'Task': 638 """Update scanner settings.""" 639 return Three.update_settings(self, advanced, camera, capture, i18n, projector, style, turntable, tutorials, viewer, software) 640 641 def upload_project(self, buffer: 'bytes') -> 'Task': 642 """Upload a project to the scanner.""" 643 return Three.upload_project(self, buffer)
Main class to manage and communicate with the Matter and Form THREE 3D Scanner via websocket.
Attributes: * OnTask (Optional[Callable[[Task], None]]): Callback for any task related messages, default is None. Will fire for task complete, progress, and error. * OnMessage (Optional[Callable[[str], None]]): Callback function to handle messages, default is None. Messages are any calls related to the system that are not tasks or buffers. Eg. {"Message":{"HasTurntable":true}} * OnBuffer (Optional[Callable[[Any, bytes], None]]): Callback Function to handle buffer data, default is None. Buffers can be image or scan data. These are binary formats that are sent separately from tasks. Buffers will contain a header that describes the buffer data size. Please note that websocket buffers are limited in size and need to be sent in chunks.
67 def __init__(self, 68 OnTask: Optional[Callable[[Task], None]] = None, 69 OnMessage: Optional[Callable[[str], None]] = None, 70 OnBuffer: Optional[Callable[[Any, bytes], None]] = None, 71 ): 72 """ 73 Initializes the Scanner object. 74 75 Args: 76 * OnTask (Optional[Callable[[Task], None]]): Callback for any task related messages, default is None. Will fire for task complete, progress, and error. 77 * OnMessage (Optional[Callable[[str], None]]): Callback function to handle messages, default is None. Messages are any calls related to the system that are not tasks or buffers. Eg. {"Message":{"HasTurntable":true}} 78 * OnBuffer (Optional[Callable[[Any, bytes], None]]): Callback Function to handle buffer data, default is None. Buffers can be image or scan data. These are binary formats that are sent separately from tasks. Buffers will contain a header that describes the buffer data size. Please note that websocket buffers are limited in size and need to be sent in chunks. 79 """ 80 self.__isConnected = False 81 82 self.OnTask = OnTask 83 self.OnMessage = OnMessage 84 self.OnBuffer = OnBuffer 85 86 self.__task_return_event = threading.Event() 87 88 # Dynamically add methods from Three to Scanner 89 # self._add_three_methods()
Initializes the Scanner object.
Args: * OnTask (Optional[Callable[[Task], None]]): Callback for any task related messages, default is None. Will fire for task complete, progress, and error. * OnMessage (Optional[Callable[[str], None]]): Callback function to handle messages, default is None. Messages are any calls related to the system that are not tasks or buffers. Eg. {"Message":{"HasTurntable":true}} * OnBuffer (Optional[Callable[[Any, bytes], None]]): Callback Function to handle buffer data, default is None. Buffers can be image or scan data. These are binary formats that are sent separately from tasks. Buffers will contain a header that describes the buffer data size. Please note that websocket buffers are limited in size and need to be sent in chunks.
100 def Connect(self, URI:str, timeoutSec=5) -> bool: 101 """ 102 Attempts to connect to the scanner using the specified URI and timeout. 103 104 Args: 105 * URI (str): The URI of the websocket server. 106 * timeoutSec (int): Timeout in seconds, default is 5. 107 108 Returns: 109 bool: True if connection is successful, raises Exception otherwise. 110 111 Raises: 112 Exception: If connection fails within the timeout or due to an error. 113 """ 114 print('Connecting to: ', URI) 115 self.__URI = URI 116 self.__isConnected = False 117 self.__error = None 118 119 self.__serverVersion__= None 120 121 self.websocket = websocket.WebSocketApp(self.__URI, 122 on_open=self.__OnOpen, 123 on_close=self.__OnClose, 124 on_error=self.__OnError, 125 on_message=self.__OnMessage, 126 ) 127 128 wst = threading.Thread(target=self.websocket.run_forever) 129 wst.daemon = True 130 wst.start() 131 132 # Wait for connection 133 start = time.time() 134 while time.time() < start + timeoutSec: 135 if self.__isConnected: 136 # Not checking versions => return True 137 return True 138 elif self.__error: 139 raise Exception(self.__error) 140 time.sleep(0.1) 141 142 raise Exception('Connection timeout')
Attempts to connect to the scanner using the specified URI and timeout.
Args: * URI (str): The URI of the websocket server. * timeoutSec (int): Timeout in seconds, default is 5.
Returns: bool: True if connection is successful, raises Exception otherwise.
Raises: Exception: If connection fails within the timeout or due to an error.
144 def Disconnect(self) -> None: 145 """ 146 Close the websocket connection. 147 """ 148 if self.__isConnected: 149 # Close the connection 150 self.websocket.close() 151 # Wait for the connection to be closed. 152 while self.__isConnected: 153 time.sleep(0.1)
Close the websocket connection.
155 def IsConnected(self)-> bool: 156 """ 157 Checks if the scanner is currently connected. 158 159 Returns: 160 bool: True if connected, False otherwise. 161 """ 162 return self.__isConnected
Checks if the scanner is currently connected.
Returns: bool: True if connected, False otherwise.
288 def SendTask(self, task, buffer:bytes = None) -> Any: 289 """ 290 Sends a task to the scanner. 291 Tasks are general control requests for the scanner. (eg. Camera exposure, or Get Image) 292 293 Creates a task, serializes it, and sends it via the websocket. 294 295 Args: 296 * task (Task): The task to send. 297 * buffer (bytes): The buffer data to send, default is None. 298 299 Returns: 300 Any: The task object that was sent. 301 302 Raises: 303 AssertionError: If the connection is not established. 304 """ 305 assert self.__isConnected 306 307 # Update the index 308 task.Index = self.__taskIndex 309 task.Input.Index = self.__taskIndex 310 self.__taskIndex += 1 311 312 # Send the task 313 self.__task_return_event.clear() 314 315 # Append the task 316 self.__tasks.append(task) 317 318 if buffer == None: 319 self.__SendTask(task) 320 else: 321 self.__SendTaskWithBuffer(task, buffer) 322 323 if task.Output: 324 # Wait for response 325 self.__task_return_event.wait() 326 327 self.__tasks.remove(task) 328 329 return task
Sends a task to the scanner. Tasks are general control requests for the scanner. (eg. Camera exposure, or Get Image)
Creates a task, serializes it, and sends it via the websocket.
Args: * task (Task): The task to send. * buffer (bytes): The buffer data to send, default is None.
Returns: Any: The task object that was sent.
Raises: AssertionError: If the connection is not established.
385 def add_merge_to_project(self) -> 'Task': 386 """Add a merged scan to the current project.""" 387 return Three.add_merge_to_project(self)
Add a merged scan to the current project.
389 def align(self, source: 'int', target: 'int', rough: 'Align.Rough' = None, fine: 'Align.Fine' = None) -> 'Task': 390 """Align two scan groups.""" 391 return Three.align(self, source, target, rough, fine)
Align two scan groups.
393 def auto_focus(self, applyAll: 'bool', cameras: 'list[AutoFocus.Camera]' = None) -> 'Task': 394 """Auto focus one or both cameras.""" 395 return Three.auto_focus(self, applyAll, cameras)
Auto focus one or both cameras.
397 def bounding_box(self, selection: 'ScanSelection', axisAligned: 'bool') -> 'Task': 398 """Get the bounding box of a set of scan groups.""" 399 return Three.bounding_box(self, selection, axisAligned)
Get the bounding box of a set of scan groups.
401 def calibrate_cameras(self) -> 'Task': 402 """Calibrate the cameras.""" 403 return Three.calibrate_cameras(self)
Calibrate the cameras.
405 def calibrate_turntable(self) -> 'Task': 406 """Calibrate the turntable.""" 407 return Three.calibrate_turntable(self)
Calibrate the turntable.
409 def calibration_capture_targets(self) -> 'Task': 410 """Get the calibration capture target for each camera calibration capture.""" 411 return Three.calibration_capture_targets(self)
Get the calibration capture target for each camera calibration capture.
413 def camera_calibration(self) -> 'Task': 414 """Get the camera calibration descriptor.""" 415 return Three.camera_calibration(self)
Get the camera calibration descriptor.
417 def capture_image(self, selection: 'list[int]' = None, codec: 'CaptureImage.Codec' = None, grayscale: 'bool' = None) -> 'Task': 418 """Capture a single Image.""" 419 return Three.capture_image(self, selection, codec, grayscale)
Capture a single Image.
421 def clear_settings(self) -> 'Task': 422 """Clear scanner settings and restore the default values.""" 423 return Three.clear_settings(self)
Clear scanner settings and restore the default values.
425 def close_project(self) -> 'Task': 426 """Close the current open project.""" 427 return Three.close_project(self)
Close the current open project.
429 def connect_wifi(self, ssid: 'str', password: 'str') -> 'Task': 430 """Connect to a wifi network.""" 431 return Three.connect_wifi(self, ssid, password)
Connect to a wifi network.
433 def copy_groups(self, sourceIndexes: 'list[int]' = None, targetIndex: 'int' = None, childPosition: 'int' = None, nameSuffix: 'str' = None, enumerate: 'bool' = None) -> 'Task': 434 """Copy a set of scan groups in the current open project.""" 435 return Three.copy_groups(self, sourceIndexes, targetIndex, childPosition, nameSuffix, enumerate)
Copy a set of scan groups in the current open project.
437 def depth_map(self, camera: 'Camera' = None, projector: 'Projector' = None, turntable: 'Turntable' = None, capture: 'Capture' = None, processing: 'Scan.Processing' = None, alignWithScanner: 'bool' = None, centerAtOrigin: 'bool' = None) -> 'Task': 438 """Capture a depth map.""" 439 return Three.depth_map(self, camera, projector, turntable, capture, processing, alignWithScanner, centerAtOrigin)
Capture a depth map.
441 def detect_calibration_card(self, Input: 'int') -> 'Task': 442 """Detect the calibration card on one or both cameras.""" 443 return Three.detect_calibration_card(self, Input)
Detect the calibration card on one or both cameras.
445 def download_project(self, Input: 'int') -> 'Task': 446 """Download a project from the scanner.""" 447 return Three.download_project(self, Input)
Download a project from the scanner.
449 def export(self, selection: 'ScanSelection' = None, texture: 'bool' = None, merge: 'bool' = None, format: 'Export.Format' = None, scale: 'float' = None, color: 'Export.Color' = None) -> 'Task': 450 """Export a group of scans.""" 451 return Three.export(self, selection, texture, merge, format, scale, color)
Export a group of scans.
453 def export_factory_calibration_logs(self) -> 'Task': 454 """Export factory calibration logs.""" 455 return Three.export_factory_calibration_logs(self)
Export factory calibration logs.
457 def export_heat_map(self, selection: 'ScanSelection' = None, texture: 'bool' = None, merge: 'bool' = None, format: 'Export.Format' = None, scale: 'float' = None, color: 'Export.Color' = None) -> 'Task': 458 """Export a mesh with vertex colors generated by the 'HeatMap' task.""" 459 return Three.export_heat_map(self, selection, texture, merge, format, scale, color)
Export a mesh with vertex colors generated by the 'HeatMap' task.
461 def export_logs(self, Input: 'bool' = None) -> 'Task': 462 """Export scanner logs.""" 463 return Three.export_logs(self, Input)
Export scanner logs.
465 def export_merge(self, selection: 'ScanSelection' = None, texture: 'bool' = None, merge: 'bool' = None, format: 'Export.Format' = None, scale: 'float' = None, color: 'Export.Color' = None) -> 'Task': 466 """Export a merged scan.""" 467 return Three.export_merge(self, selection, texture, merge, format, scale, color)
Export a merged scan.
469 def factory_reset(self) -> 'Task': 470 """Reset the scanner to factory settings.""" 471 return Three.factory_reset(self)
Reset the scanner to factory settings.
473 def flatten_group(self, Input: 'int') -> 'Task': 474 """Flatten a scan group such that it only consists of single scans.""" 475 return Three.flatten_group(self, Input)
Flatten a scan group such that it only consists of single scans.
477 def forget_wifi(self) -> 'Task': 478 """Forget all wifi connections.""" 479 return Three.forget_wifi(self)
Forget all wifi connections.
481 def has_cameras(self) -> 'Task': 482 """Check if the scanner has working cameras.""" 483 return Three.has_cameras(self)
Check if the scanner has working cameras.
485 def has_projector(self) -> 'Task': 486 """Check if the scanner has a working projector.""" 487 return Three.has_projector(self)
Check if the scanner has a working projector.
489 def has_turntable(self) -> 'Task': 490 """Check if the scanner is connected to a working turntable.""" 491 return Three.has_turntable(self)
Check if the scanner is connected to a working turntable.
493 def heat_map(self, sources: 'list[int]' = None, targets: 'list[int]' = None, outlierDistance: 'float' = None) -> 'Task': 494 """Compute the point-to-mesh distances of a source mesh to a target mesh and visualize as a heat map.""" 495 return Three.heat_map(self, sources, targets, outlierDistance)
Compute the point-to-mesh distances of a source mesh to a target mesh and visualize as a heat map.
497 def import_file(self, name: 'str' = None, scale: 'float' = None, unit: 'Import.Unit' = None, center: 'bool' = None, groupIndex: 'int' = None) -> 'Task': 498 """Import a set of 3D meshes to the current open project. The meshes must be archived in a ZIP file.""" 499 return Three.import_file(self, name, scale, unit, center, groupIndex)
Import a set of 3D meshes to the current open project. The meshes must be archived in a ZIP file.
501 def list_export_formats(self) -> 'Task': 502 """List all export formats.""" 503 return Three.list_export_formats(self)
List all export formats.
505 def list_groups(self) -> 'Task': 506 """List the scan groups in the current open project.""" 507 return Three.list_groups(self)
List the scan groups in the current open project.
509 def list_network_interfaces(self) -> 'Task': 510 """List available wifi networks.""" 511 return Three.list_network_interfaces(self)
List available wifi networks.
513 def list_projects(self) -> 'Task': 514 """List all projects.""" 515 return Three.list_projects(self)
List all projects.
517 def list_scans(self) -> 'Task': 518 """List the scans in the current open project.""" 519 return Three.list_scans(self)
List the scans in the current open project.
521 def list_settings(self) -> 'Task': 522 """Get scanner settings.""" 523 return Three.list_settings(self)
Get scanner settings.
525 def list_wifi(self) -> 'Task': 526 """List available wifi networks.""" 527 return Three.list_wifi(self)
List available wifi networks.
529 def merge(self, selection: 'ScanSelection' = None, remesh: 'Merge.Remesh' = None, simplify: 'Merge.Simplify' = None, texturize: 'bool' = None) -> 'Task': 530 """Merge two or more scan groups.""" 531 return Three.merge(self, selection, remesh, simplify, texturize)
Merge two or more scan groups.
533 def merge_data(self, index: 'int', mergeStep: 'ScanData.MergeStep' = None, buffers: 'list[ScanData.Buffer]' = None, metadata: 'list[ScanData.Metadata]' = None) -> 'Task': 534 """Download the raw scan data for the current merge process.""" 535 return Three.merge_data(self, index, mergeStep, buffers, metadata)
Download the raw scan data for the current merge process.
537 def move_group(self, Input: 'list[int]' = None) -> 'Task': 538 """Move a scan group.""" 539 return Three.move_group(self, Input)
Move a scan group.
541 def new_group(self, parentIndex: 'int' = None, baseName: 'str' = None, color: 'list[float]' = None, visible: 'bool' = None, collapsed: 'bool' = None, rotation: 'list[float]' = None, translation: 'list[float]' = None) -> 'Task': 542 """Create a new scan group.""" 543 return Three.new_group(self, parentIndex, baseName, color, visible, collapsed, rotation, translation)
Create a new scan group.
545 def new_project(self, Input: 'str' = None) -> 'Task': 546 """Create a new project.""" 547 return Three.new_project(self, Input)
Create a new project.
549 def new_scan(self, camera: 'Camera' = None, projector: 'Projector' = None, turntable: 'Turntable' = None, capture: 'Capture' = None, processing: 'Scan.Processing' = None, alignWithScanner: 'bool' = None, centerAtOrigin: 'bool' = None) -> 'Task': 550 """Capture a new scan.""" 551 return Three.new_scan(self, camera, projector, turntable, capture, processing, alignWithScanner, centerAtOrigin)
Capture a new scan.
553 def open_project(self, Input: 'int') -> 'Task': 554 """Open an existing project.""" 555 return Three.open_project(self, Input)
Open an existing project.
557 def pop_settings(self, Input: 'bool' = None) -> 'Task': 558 """Pop and restore scanner settings from the settings stack.""" 559 return Three.pop_settings(self, Input)
Pop and restore scanner settings from the settings stack.
561 def push_settings(self) -> 'Task': 562 """Push the current scanner settings to the settings stack.""" 563 return Three.push_settings(self)
Push the current scanner settings to the settings stack.
569 def remove_groups(self, Input: 'list[int]' = None) -> 'Task': 570 """Remove selected scan groups.""" 571 return Three.remove_groups(self, Input)
Remove selected scan groups.
573 def remove_projects(self, Input: 'list[int]' = None) -> 'Task': 574 """Remove selected projects.""" 575 return Three.remove_projects(self, Input)
Remove selected projects.
577 def restore_factory_calibration(self) -> 'Task': 578 """Restore factory calibration.""" 579 return Three.restore_factory_calibration(self)
Restore factory calibration.
581 def rotate_turntable(self, Input: 'int') -> 'Task': 582 """Rotate the turntable.""" 583 return Three.rotate_turntable(self, Input)
Rotate the turntable.
585 def scan_data(self, index: 'int', mergeStep: 'ScanData.MergeStep' = None, buffers: 'list[ScanData.Buffer]' = None, metadata: 'list[ScanData.Metadata]' = None) -> 'Task': 586 """Download the raw scan data for a scan in the current open project.""" 587 return Three.scan_data(self, index, mergeStep, buffers, metadata)
Download the raw scan data for a scan in the current open project.
589 def set_cameras(self, selection: 'list[int]' = None, autoExposure: 'bool' = None, exposure: 'int' = None, analogGain: 'float' = None, digitalGain: 'int' = None, focus: 'int' = None) -> 'Task': 590 """Apply camera settings to one or both cameras.""" 591 return Three.set_cameras(self, selection, autoExposure, exposure, analogGain, digitalGain, focus)
Apply camera settings to one or both cameras.
593 def set_group(self, index: 'int', name: 'str' = None, color: 'list[float]' = None, visible: 'bool' = None, collapsed: 'bool' = None, rotation: 'list[float]' = None, translation: 'list[float]' = None) -> 'Task': 594 """Set scan group properties.""" 595 return Three.set_group(self, index, name, color, visible, collapsed, rotation, translation)
Set scan group properties.
597 def set_project(self, index: 'int' = None, name: 'str' = None) -> 'Task': 598 """Apply settings to the current open project.""" 599 return Three.set_project(self, index, name)
Apply settings to the current open project.
601 def set_projector(self, on: 'bool' = None, brightness: 'float' = None, pattern: 'Projector.Pattern' = None, image: 'Projector.Image' = None, color: 'list[float]' = None, buffer: 'bytes' = None) -> 'Task': 602 """Apply projector settings.""" 603 return Three.set_projector(self, on, brightness, pattern, image, color, buffer)
Apply projector settings.
609 def smooth(self, selection: 'ScanSelection' = None, taubin: 'Smooth.Taubin' = None) -> 'Task': 610 """Smooth a set of scans.""" 611 return Three.smooth(self, selection, taubin)
Smooth a set of scans.
613 def split_group(self, Input: 'int') -> 'Task': 614 """Split a scan group (ie. move its subgroups to its parent group).""" 615 return Three.split_group(self, Input)
Split a scan group (ie. move its subgroups to its parent group).
617 def start_video(self) -> 'Task': 618 """Start the video stream.""" 619 return Three.start_video(self)
Start the video stream.
621 def stop_video(self) -> 'Task': 622 """Stop the video stream.""" 623 return Three.stop_video(self)
Stop the video stream.
625 def system_info(self, updateMajor: 'bool' = None, updateNightly: 'bool' = None) -> 'Task': 626 """Get system information.""" 627 return Three.system_info(self, updateMajor, updateNightly)
Get system information.
629 def transform_group(self, index: 'int', name: 'str' = None, color: 'list[float]' = None, visible: 'bool' = None, collapsed: 'bool' = None, rotation: 'list[float]' = None, translation: 'list[float]' = None) -> 'Task': 630 """Apply a rigid transformation to a group.""" 631 return Three.transform_group(self, index, name, color, visible, collapsed, rotation, translation)
Apply a rigid transformation to a group.
633 def turntable_calibration(self) -> 'Task': 634 """Get the turntable calibration descriptor.""" 635 return Three.turntable_calibration(self)
Get the turntable calibration descriptor.
637 def update_settings(self, advanced: 'Advanced' = None, camera: 'Camera' = None, capture: 'Capture' = None, i18n: 'I18n' = None, projector: 'Projector' = None, style: 'Style' = None, turntable: 'Turntable' = None, tutorials: 'Tutorials' = None, viewer: 'Viewer' = None, software: 'Software' = None) -> 'Task': 638 """Update scanner settings.""" 639 return Three.update_settings(self, advanced, camera, capture, i18n, projector, style, turntable, tutorials, viewer, software)
Update scanner settings.