Snapshot.py 5.1 KB

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