Snapshot.py 5.2 KB

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