Snapshot.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. # Copyright (c) 2021 Ultimaker B.V.
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. import numpy
  4. from PyQt6 import QtCore
  5. from PyQt6.QtCore import QCoreApplication
  6. from PyQt6.QtGui import QImage
  7. from UM.Logger import Logger
  8. from cura.PreviewPass import PreviewPass
  9. from UM.Application import Application
  10. from UM.Math.Matrix import Matrix
  11. from UM.Math.Vector import Vector
  12. from UM.Scene.Camera import Camera
  13. from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
  14. class Snapshot:
  15. @staticmethod
  16. def getImageBoundaries(image: QImage):
  17. # Look at the resulting image to get a good crop.
  18. # Get the pixels as byte array
  19. pixel_array = image.bits().asarray(image.sizeInBytes())
  20. width, height = image.width(), image.height()
  21. # Convert to numpy array, assume it's 32 bit (it should always be)
  22. pixels = numpy.frombuffer(pixel_array, dtype=numpy.uint8).reshape([height, width, 4])
  23. # Find indices of non zero pixels
  24. nonzero_pixels = numpy.nonzero(pixels)
  25. min_y, min_x, min_a_ = numpy.amin(nonzero_pixels, axis=1) # type: ignore
  26. max_y, max_x, max_a_ = numpy.amax(nonzero_pixels, axis=1) # type: ignore
  27. return min_x, max_x, min_y, max_y
  28. @staticmethod
  29. def snapshot(width = 300, height = 300):
  30. """Return a QImage of the scene
  31. Uses PreviewPass that leaves out some elements Aspect ratio assumes a square
  32. :param width: width of the aspect ratio default 300
  33. :param height: height of the aspect ratio default 300
  34. :return: None when there is no model on the build plate otherwise it will return an image
  35. """
  36. scene = Application.getInstance().getController().getScene()
  37. active_camera = scene.getActiveCamera() or scene.findCamera("3d")
  38. render_width, render_height = (width, height) if active_camera is None else active_camera.getWindowSize()
  39. render_width = int(render_width)
  40. render_height = int(render_height)
  41. QCoreApplication.processEvents() # This ensures that the opengl context is correctly available
  42. preview_pass = PreviewPass(render_width, render_height)
  43. root = scene.getRoot()
  44. camera = Camera("snapshot", root)
  45. # determine zoom and look at
  46. bbox = None
  47. for node in DepthFirstIterator(root):
  48. if not getattr(node, "_outside_buildarea", False):
  49. if node.callDecoration("isSliceable") and node.getMeshData() and node.isVisible() and not node.callDecoration("isNonThumbnailVisibleMesh"):
  50. if bbox is None:
  51. bbox = node.getBoundingBox()
  52. else:
  53. bbox = bbox + node.getBoundingBox()
  54. # If there is no bounding box, it means that there is no model in the buildplate
  55. if bbox is None:
  56. Logger.log("w", "Unable to create snapshot as we seem to have an empty buildplate")
  57. return None
  58. look_at = bbox.center
  59. # guessed size so the objects are hopefully big
  60. size = max(bbox.width, bbox.height, bbox.depth * 0.5)
  61. # Looking from this direction (x, y, z) in OGL coordinates
  62. looking_from_offset = Vector(-1, 1, 2)
  63. if size > 0:
  64. # determine the watch distance depending on the size
  65. looking_from_offset = looking_from_offset * size * 1.75
  66. camera.setPosition(look_at + looking_from_offset)
  67. camera.lookAt(look_at)
  68. satisfied = False
  69. size = None
  70. fovy = 30
  71. while not satisfied:
  72. if size is not None:
  73. satisfied = True # always be satisfied after second try
  74. projection_matrix = Matrix()
  75. # Somehow the aspect ratio is also influenced in reverse by the screen width/height
  76. # So you have to set it to render_width/render_height to get 1
  77. projection_matrix.setPerspective(fovy, render_width / render_height, 1, 500)
  78. camera.setProjectionMatrix(projection_matrix)
  79. preview_pass.setCamera(camera)
  80. preview_pass.render()
  81. pixel_output = preview_pass.getOutput()
  82. try:
  83. min_x, max_x, min_y, max_y = Snapshot.getImageBoundaries(pixel_output)
  84. except (ValueError, AttributeError):
  85. Logger.logException("w", "Failed to crop the snapshot!")
  86. return None
  87. size = max((max_x - min_x) / render_width, (max_y - min_y) / render_height)
  88. if size > 0.5 or satisfied:
  89. satisfied = True
  90. else:
  91. # make it big and allow for some empty space around
  92. fovy *= 0.5 # strangely enough this messes up the aspect ratio: fovy *= size * 1.1
  93. # make it a square
  94. if max_x - min_x >= max_y - min_y:
  95. # make y bigger
  96. min_y, max_y = int((max_y + min_y) / 2 - (max_x - min_x) / 2), int((max_y + min_y) / 2 + (max_x - min_x) / 2)
  97. else:
  98. # make x bigger
  99. min_x, max_x = int((max_x + min_x) / 2 - (max_y - min_y) / 2), int((max_x + min_x) / 2 + (max_y - min_y) / 2)
  100. cropped_image = pixel_output.copy(min_x, min_y, max_x - min_x, max_y - min_y)
  101. # Scale it to the correct size
  102. scaled_image = cropped_image.scaled(
  103. width, height,
  104. aspectRatioMode = QtCore.Qt.AspectRatioMode.IgnoreAspectRatio,
  105. transformMode = QtCore.Qt.TransformationMode.SmoothTransformation)
  106. return scaled_image