ImageReader.py 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. # Copyright (c) 2020 Ultimaker B.V.
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. import numpy
  4. import math
  5. from PyQt5.QtGui import QImage, qRed, qGreen, qBlue, qAlpha
  6. from PyQt5.QtCore import Qt
  7. from UM.Mesh.MeshReader import MeshReader
  8. from UM.Mesh.MeshBuilder import MeshBuilder
  9. from UM.Math.Vector import Vector
  10. from UM.Job import Job
  11. from UM.Logger import Logger
  12. from .ImageReaderUI import ImageReaderUI
  13. from cura.Scene.CuraSceneNode import CuraSceneNode as SceneNode
  14. class ImageReader(MeshReader):
  15. def __init__(self) -> None:
  16. super().__init__()
  17. self._supported_extensions = [".jpg", ".jpeg", ".bmp", ".gif", ".png"]
  18. self._ui = ImageReaderUI(self)
  19. def preRead(self, file_name, *args, **kwargs):
  20. img = QImage(file_name)
  21. if img.isNull():
  22. Logger.log("e", "Image is corrupt.")
  23. return MeshReader.PreReadResult.failed
  24. width = img.width()
  25. depth = img.height()
  26. largest = max(width, depth)
  27. width = width / largest * self._ui.default_width
  28. depth = depth / largest * self._ui.default_depth
  29. self._ui.setWidthAndDepth(width, depth)
  30. self._ui.showConfigUI()
  31. self._ui.waitForUIToClose()
  32. if self._ui.getCancelled():
  33. return MeshReader.PreReadResult.cancelled
  34. return MeshReader.PreReadResult.accepted
  35. def _read(self, file_name):
  36. size = max(self._ui.getWidth(), self._ui.getDepth())
  37. return self._generateSceneNode(file_name, size, self._ui.peak_height, self._ui.base_height, self._ui.smoothing, 512, self._ui.lighter_is_higher, self._ui.use_transparency_model, self._ui.transmittance_1mm)
  38. def _generateSceneNode(self, file_name, xz_size, height_from_base, base_height, blur_iterations, max_size, lighter_is_higher, use_transparency_model, transmittance_1mm):
  39. scene_node = SceneNode()
  40. mesh = MeshBuilder()
  41. img = QImage(file_name)
  42. if img.isNull():
  43. Logger.log("e", "Image is corrupt.")
  44. return None
  45. width = max(img.width(), 2)
  46. height = max(img.height(), 2)
  47. aspect = height / width
  48. if img.width() < 2 or img.height() < 2:
  49. img = img.scaled(width, height, Qt.IgnoreAspectRatio)
  50. height_from_base = max(height_from_base, 0)
  51. base_height = max(base_height, 0)
  52. peak_height = base_height + height_from_base
  53. xz_size = max(xz_size, 1)
  54. scale_vector = Vector(xz_size, peak_height, xz_size)
  55. if width > height:
  56. scale_vector = scale_vector.set(z=scale_vector.z * aspect)
  57. elif height > width:
  58. scale_vector = scale_vector.set(x=scale_vector.x / aspect)
  59. if width > max_size or height > max_size:
  60. scale_factor = max_size / width
  61. if height > width:
  62. scale_factor = max_size / height
  63. width = int(max(round(width * scale_factor), 2))
  64. height = int(max(round(height * scale_factor), 2))
  65. img = img.scaled(width, height, Qt.IgnoreAspectRatio)
  66. width_minus_one = width - 1
  67. height_minus_one = height - 1
  68. Job.yieldThread()
  69. texel_width = 1.0 / (width_minus_one) * scale_vector.x
  70. texel_height = 1.0 / (height_minus_one) * scale_vector.z
  71. height_data = numpy.zeros((height, width), dtype = numpy.float32)
  72. for x in range(0, width):
  73. for y in range(0, height):
  74. qrgb = img.pixel(x, y)
  75. if use_transparency_model:
  76. height_data[y, x] = (0.299 * math.pow(qRed(qrgb) / 255.0, 2.2) + 0.587 * math.pow(qGreen(qrgb) / 255.0, 2.2) + 0.114 * math.pow(qBlue(qrgb) / 255.0, 2.2))
  77. else:
  78. height_data[y, x] = (0.212655 * qRed(qrgb) + 0.715158 * qGreen(qrgb) + 0.072187 * qBlue(qrgb)) / 255 # fast computation ignoring gamma and degamma
  79. Job.yieldThread()
  80. if lighter_is_higher == use_transparency_model:
  81. height_data = 1 - height_data
  82. for _ in range(0, blur_iterations):
  83. copy = numpy.pad(height_data, ((1, 1), (1, 1)), mode = "edge")
  84. height_data += copy[1:-1, 2:]
  85. height_data += copy[1:-1, :-2]
  86. height_data += copy[2:, 1:-1]
  87. height_data += copy[:-2, 1:-1]
  88. height_data += copy[2:, 2:]
  89. height_data += copy[:-2, 2:]
  90. height_data += copy[2:, :-2]
  91. height_data += copy[:-2, :-2]
  92. height_data /= 9
  93. Job.yieldThread()
  94. if use_transparency_model:
  95. divisor = 1.0 / math.log(transmittance_1mm / 100.0) # log-base doesn't matter here. Precompute this value for faster computation of each pixel.
  96. min_luminance = (transmittance_1mm / 100.0) ** (peak_height - base_height)
  97. for (y, x) in numpy.ndindex(height_data.shape):
  98. mapped_luminance = min_luminance + (1.0 - min_luminance) * height_data[y, x]
  99. height_data[y, x] = base_height + divisor * math.log(mapped_luminance) # use same base as a couple lines above this
  100. else:
  101. height_data *= scale_vector.y
  102. height_data += base_height
  103. if img.hasAlphaChannel():
  104. for x in range(0, width):
  105. for y in range(0, height):
  106. height_data[y, x] *= qAlpha(img.pixel(x, y)) / 255.0
  107. heightmap_face_count = 2 * height_minus_one * width_minus_one
  108. total_face_count = heightmap_face_count + (width_minus_one * 2) * (height_minus_one * 2) + 2
  109. mesh.reserveFaceCount(total_face_count)
  110. # initialize to texel space vertex offsets.
  111. # 6 is for 6 vertices for each texel quad.
  112. heightmap_vertices = numpy.zeros((width_minus_one * height_minus_one, 6, 3), dtype = numpy.float32)
  113. heightmap_vertices = heightmap_vertices + numpy.array([[
  114. [0, base_height, 0],
  115. [0, base_height, texel_height],
  116. [texel_width, base_height, texel_height],
  117. [texel_width, base_height, texel_height],
  118. [texel_width, base_height, 0],
  119. [0, base_height, 0]
  120. ]], dtype = numpy.float32)
  121. offsetsz, offsetsx = numpy.mgrid[0: height_minus_one, 0: width - 1]
  122. offsetsx = numpy.array(offsetsx, numpy.float32).reshape(-1, 1) * texel_width
  123. offsetsz = numpy.array(offsetsz, numpy.float32).reshape(-1, 1) * texel_height
  124. # offsets for each texel quad
  125. heightmap_vertex_offsets = numpy.concatenate([offsetsx, numpy.zeros((offsetsx.shape[0], offsetsx.shape[1]), dtype = numpy.float32), offsetsz], 1)
  126. heightmap_vertices += heightmap_vertex_offsets.repeat(6, 0).reshape(-1, 6, 3)
  127. # apply height data to y values
  128. heightmap_vertices[:, 0, 1] = heightmap_vertices[:, 5, 1] = height_data[:-1, :-1].reshape(-1)
  129. heightmap_vertices[:, 1, 1] = height_data[1:, :-1].reshape(-1)
  130. heightmap_vertices[:, 2, 1] = heightmap_vertices[:, 3, 1] = height_data[1:, 1:].reshape(-1)
  131. heightmap_vertices[:, 4, 1] = height_data[:-1, 1:].reshape(-1)
  132. heightmap_indices = numpy.array(numpy.mgrid[0:heightmap_face_count * 3], dtype = numpy.int32).reshape(-1, 3)
  133. mesh._vertices[0:(heightmap_vertices.size // 3), :] = heightmap_vertices.reshape(-1, 3)
  134. mesh._indices[0:(heightmap_indices.size // 3), :] = heightmap_indices
  135. mesh._vertex_count = heightmap_vertices.size // 3
  136. mesh._face_count = heightmap_indices.size // 3
  137. geo_width = width_minus_one * texel_width
  138. geo_height = height_minus_one * texel_height
  139. # bottom
  140. mesh.addFaceByPoints(0, 0, 0, 0, 0, geo_height, geo_width, 0, geo_height)
  141. mesh.addFaceByPoints(geo_width, 0, geo_height, geo_width, 0, 0, 0, 0, 0)
  142. # north and south walls
  143. for n in range(0, width_minus_one):
  144. x = n * texel_width
  145. nx = (n + 1) * texel_width
  146. hn0 = height_data[0, n]
  147. hn1 = height_data[0, n + 1]
  148. hs0 = height_data[height_minus_one, n]
  149. hs1 = height_data[height_minus_one, n + 1]
  150. mesh.addFaceByPoints(x, 0, 0, nx, 0, 0, nx, hn1, 0)
  151. mesh.addFaceByPoints(nx, hn1, 0, x, hn0, 0, x, 0, 0)
  152. mesh.addFaceByPoints(x, 0, geo_height, nx, 0, geo_height, nx, hs1, geo_height)
  153. mesh.addFaceByPoints(nx, hs1, geo_height, x, hs0, geo_height, x, 0, geo_height)
  154. # west and east walls
  155. for n in range(0, height_minus_one):
  156. y = n * texel_height
  157. ny = (n + 1) * texel_height
  158. hw0 = height_data[n, 0]
  159. hw1 = height_data[n + 1, 0]
  160. he0 = height_data[n, width_minus_one]
  161. he1 = height_data[n + 1, width_minus_one]
  162. mesh.addFaceByPoints(0, 0, y, 0, 0, ny, 0, hw1, ny)
  163. mesh.addFaceByPoints(0, hw1, ny, 0, hw0, y, 0, 0, y)
  164. mesh.addFaceByPoints(geo_width, 0, y, geo_width, 0, ny, geo_width, he1, ny)
  165. mesh.addFaceByPoints(geo_width, he1, ny, geo_width, he0, y, geo_width, 0, y)
  166. mesh.calculateNormals(fast = True)
  167. scene_node.setMeshData(mesh.build())
  168. return scene_node