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