Snapshot.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  1. # Copyright (c) 2018 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.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)
  24. max_y, max_x, max_a_ = numpy.amax(nonzero_pixels, axis=1)
  25. return min_x, max_x, min_y, max_y
  26. ## Return a QImage of the scene
  27. # Uses PreviewPass that leaves out some elements
  28. # Aspect ratio assumes a square
  29. @staticmethod
  30. def snapshot(width = 300, height = 300):
  31. scene = Application.getInstance().getController().getScene()
  32. active_camera = scene.getActiveCamera()
  33. render_width, render_height = active_camera.getWindowSize()
  34. render_width = int(render_width)
  35. render_height = int(render_height)
  36. preview_pass = PreviewPass(render_width, render_height)
  37. root = scene.getRoot()
  38. camera = Camera("snapshot", root)
  39. # determine zoom and look at
  40. bbox = None
  41. for node in DepthFirstIterator(root):
  42. if not getattr(node, "_outside_buildarea", False):
  43. if node.callDecoration("isSliceable") and node.getMeshData() and node.isVisible() and not node.callDecoration("isNonThumbnailVisibleMesh"):
  44. if bbox is None:
  45. bbox = node.getBoundingBox()
  46. else:
  47. bbox = bbox + node.getBoundingBox()
  48. # If there is no bounding box, it means that there is no model in the buildplate
  49. if bbox is None:
  50. return None
  51. look_at = bbox.center
  52. # guessed size so the objects are hopefully big
  53. size = max(bbox.width, bbox.height, bbox.depth * 0.5)
  54. # Looking from this direction (x, y, z) in OGL coordinates
  55. looking_from_offset = Vector(-1, 1, 2)
  56. if size > 0:
  57. # determine the watch distance depending on the size
  58. looking_from_offset = looking_from_offset * size * 1.75
  59. camera.setPosition(look_at + looking_from_offset)
  60. camera.lookAt(look_at)
  61. satisfied = False
  62. size = None
  63. fovy = 30
  64. while not satisfied:
  65. if size is not None:
  66. satisfied = True # always be satisfied after second try
  67. projection_matrix = Matrix()
  68. # Somehow the aspect ratio is also influenced in reverse by the screen width/height
  69. # So you have to set it to render_width/render_height to get 1
  70. projection_matrix.setPerspective(fovy, render_width / render_height, 1, 500)
  71. camera.setProjectionMatrix(projection_matrix)
  72. preview_pass.setCamera(camera)
  73. preview_pass.render()
  74. pixel_output = preview_pass.getOutput()
  75. min_x, max_x, min_y, max_y = Snapshot.getImageBoundaries(pixel_output)
  76. size = max((max_x - min_x) / render_width, (max_y - min_y) / render_height)
  77. if size > 0.5 or satisfied:
  78. satisfied = True
  79. else:
  80. # make it big and allow for some empty space around
  81. fovy *= 0.5 # strangely enough this messes up the aspect ratio: fovy *= size * 1.1
  82. # make it a square
  83. if max_x - min_x >= max_y - min_y:
  84. # make y bigger
  85. 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)
  86. else:
  87. # make x bigger
  88. 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)
  89. cropped_image = pixel_output.copy(min_x, min_y, max_x - min_x, max_y - min_y)
  90. # Scale it to the correct size
  91. scaled_image = cropped_image.scaled(
  92. width, height,
  93. aspectRatioMode = QtCore.Qt.IgnoreAspectRatio,
  94. transformMode = QtCore.Qt.SmoothTransformation)
  95. return scaled_image