dataset_builder.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620
  1. import json, os, gzip, shutil
  2. from vtkmodules.vtkRenderingCore import vtkWindowToImageFilter
  3. from vtkmodules.vtkIOImage import vtkPNGReader, vtkPNGWriter, vtkJPEGWriter
  4. from vtkmodules.vtkCommonDataModel import vtkImageData
  5. from vtkmodules.vtkCommonCore import vtkUnsignedCharArray
  6. from vtkmodules.vtkFiltersParallel import vtkPResampleFilter
  7. from vtkmodules.web import iteritems, getJSArrayType
  8. from vtkmodules.web.camera import (
  9. update_camera,
  10. create_spherical_camera,
  11. create_cylindrical_camera,
  12. )
  13. from vtkmodules.web.query_data_model import DataHandler
  14. # Global helper variables
  15. encode_codes = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
  16. # -----------------------------------------------------------------------------
  17. # Capture image from render window
  18. # -----------------------------------------------------------------------------
  19. class CaptureRenderWindow(object):
  20. def __init__(self, magnification=1):
  21. self.windowToImage = vtkWindowToImageFilter()
  22. self.windowToImage.SetScale(magnification)
  23. self.windowToImage.SetInputBufferTypeToRGB()
  24. self.windowToImage.ReadFrontBufferOn()
  25. self.writer = None
  26. def SetRenderWindow(self, renderWindow):
  27. self.windowToImage.SetInput(renderWindow)
  28. def SetFormat(self, mimeType):
  29. if mimeType == "image/png":
  30. self.writer = vtkPNGWriter()
  31. self.writer.SetInputConnection(self.windowToImage.GetOutputPort())
  32. elif mimeType == "image/jpg":
  33. self.writer = vtkJPEGWriter()
  34. self.writer.SetInputConnection(self.windowToImage.GetOutputPort())
  35. def writeImage(self, path):
  36. if self.writer:
  37. self.windowToImage.Modified()
  38. self.windowToImage.Update()
  39. self.writer.SetFileName(path)
  40. self.writer.Write()
  41. # -----------------------------------------------------------------------------
  42. # Basic Dataset Builder
  43. # -----------------------------------------------------------------------------
  44. class DataSetBuilder(object):
  45. def __init__(self, location, camera_data, metadata={}, sections={}):
  46. self.dataHandler = DataHandler(location)
  47. self.cameraDescription = camera_data
  48. self.camera = None
  49. self.imageCapture = CaptureRenderWindow()
  50. for key, value in iteritems(metadata):
  51. self.dataHandler.addMetaData(key, value)
  52. for key, value in iteritems(sections):
  53. self.dataHandler.addSection(key, value)
  54. def getDataHandler(self):
  55. return self.dataHandler
  56. def getCamera(self):
  57. return self.camera
  58. def updateCamera(self, camera):
  59. update_camera(self.renderer, camera)
  60. self.renderWindow.Render()
  61. def start(self, renderWindow=None, renderer=None):
  62. if renderWindow:
  63. # Keep track of renderWindow and renderer
  64. self.renderWindow = renderWindow
  65. self.renderer = renderer
  66. # Initialize image capture
  67. self.imageCapture.SetRenderWindow(renderWindow)
  68. # Handle camera if any
  69. if self.cameraDescription:
  70. if self.cameraDescription["type"] == "spherical":
  71. self.camera = create_spherical_camera(
  72. renderer,
  73. self.dataHandler,
  74. self.cameraDescription["phi"],
  75. self.cameraDescription["theta"],
  76. )
  77. elif self.cameraDescription["type"] == "cylindrical":
  78. self.camera = create_cylindrical_camera(
  79. renderer,
  80. self.dataHandler,
  81. self.cameraDescription["phi"],
  82. self.cameraDescription["translation"],
  83. )
  84. # Update background color
  85. bgColor = renderer.GetBackground()
  86. bgColorString = "rgb(%d, %d, %d)" % tuple(
  87. int(bgColor[i] * 255) for i in range(3)
  88. )
  89. self.dataHandler.addMetaData("backgroundColor", bgColorString)
  90. # Update file patterns
  91. self.dataHandler.updateBasePattern()
  92. def stop(self):
  93. self.dataHandler.writeDataDescriptor()
  94. # -----------------------------------------------------------------------------
  95. # Image Dataset Builder
  96. # -----------------------------------------------------------------------------
  97. class ImageDataSetBuilder(DataSetBuilder):
  98. def __init__(self, location, imageMimeType, cameraInfo, metadata={}, sections={}):
  99. DataSetBuilder.__init__(self, location, cameraInfo, metadata, sections)
  100. imageExtenstion = "." + imageMimeType.split("/")[1]
  101. self.dataHandler.registerData(
  102. name="image", type="blob", mimeType=imageMimeType, fileName=imageExtenstion
  103. )
  104. self.imageCapture.SetFormat(imageMimeType)
  105. def writeImage(self):
  106. self.imageCapture.writeImage(self.dataHandler.getDataAbsoluteFilePath("image"))
  107. def writeImages(self):
  108. for cam in self.camera:
  109. update_camera(self.renderer, cam)
  110. self.renderWindow.Render()
  111. self.imageCapture.writeImage(
  112. self.dataHandler.getDataAbsoluteFilePath("image")
  113. )
  114. # -----------------------------------------------------------------------------
  115. # Volume Composite Dataset Builder
  116. # -----------------------------------------------------------------------------
  117. class VolumeCompositeDataSetBuilder(DataSetBuilder):
  118. def __init__(self, location, imageMimeType, cameraInfo, metadata={}, sections={}):
  119. DataSetBuilder.__init__(self, location, cameraInfo, metadata, sections)
  120. self.dataHandler.addTypes("volume-composite", "rgba+depth")
  121. self.imageMimeType = imageMimeType
  122. self.imageExtenstion = "." + imageMimeType.split("/")[1]
  123. if imageMimeType == "image/png":
  124. self.imageWriter = vtkPNGWriter()
  125. if imageMimeType == "image/jpg":
  126. self.imageWriter = vtkJPEGWriter()
  127. self.imageDataColor = vtkImageData()
  128. self.imageWriter.SetInputData(self.imageDataColor)
  129. self.imageDataDepth = vtkImageData()
  130. self.depthToWrite = None
  131. self.layerInfo = {}
  132. self.colorByMapping = {}
  133. self.compositePipeline = {
  134. "layers": [],
  135. "dimensions": [],
  136. "fields": {},
  137. "layer_fields": {},
  138. "pipeline": [],
  139. }
  140. self.activeDepthKey = ""
  141. self.activeRGBKey = ""
  142. self.nodeWithChildren = {}
  143. def _getColorCode(self, colorBy):
  144. if colorBy in self.colorByMapping:
  145. # The color code exist
  146. return self.colorByMapping[colorBy]
  147. else:
  148. # No color code assigned yet
  149. colorCode = encode_codes[len(self.colorByMapping)]
  150. # Assign color code
  151. self.colorByMapping[colorBy] = colorCode
  152. # Register color code with color by value
  153. self.compositePipeline["fields"][colorCode] = colorBy
  154. # Return the color code
  155. return colorCode
  156. def _getLayerCode(self, parent, layerName):
  157. if layerName in self.layerInfo:
  158. # Layer already exist
  159. return (self.layerInfo[layerName]["code"], False)
  160. else:
  161. layerCode = encode_codes[len(self.layerInfo)]
  162. self.layerInfo[layerName] = {
  163. "code": layerCode,
  164. "name": layerName,
  165. "parent": parent,
  166. }
  167. self.compositePipeline["layers"].append(layerCode)
  168. self.compositePipeline["layer_fields"][layerCode] = []
  169. # Let's register it in the pipeline
  170. if parent:
  171. if parent not in self.nodeWithChildren:
  172. # Need to create parent
  173. rootNode = {"name": parent, "ids": [], "children": []}
  174. self.nodeWithChildren[parent] = rootNode
  175. self.compositePipeline["pipeline"].append(rootNode)
  176. # Add node to its parent
  177. self.nodeWithChildren[parent]["children"].append(
  178. {"name": layerName, "ids": [layerCode]}
  179. )
  180. self.nodeWithChildren[parent]["ids"].append(layerCode)
  181. else:
  182. self.compositePipeline["pipeline"].append(
  183. {"name": layerName, "ids": [layerCode]}
  184. )
  185. return (layerCode, True)
  186. def _needToRegisterColor(self, layerCode, colorCode):
  187. if colorCode in self.compositePipeline["layer_fields"][layerCode]:
  188. return False
  189. else:
  190. self.compositePipeline["layer_fields"][layerCode].append(colorCode)
  191. return True
  192. def activateLayer(self, parent, name, colorBy):
  193. layerCode, needToRegisterDepth = self._getLayerCode(parent, name)
  194. colorCode = self._getColorCode(colorBy)
  195. needToRegisterColor = self._needToRegisterColor(layerCode, colorCode)
  196. # Update active keys
  197. self.activeDepthKey = "%s_depth" % layerCode
  198. self.activeRGBKey = "%s%s_rgb" % (layerCode, colorCode)
  199. # Need to register data
  200. if needToRegisterDepth:
  201. self.dataHandler.registerData(
  202. name=self.activeDepthKey,
  203. type="array",
  204. fileName="/%s_depth.uint8" % layerCode,
  205. categories=[layerCode],
  206. )
  207. if needToRegisterColor:
  208. self.dataHandler.registerData(
  209. name=self.activeRGBKey,
  210. type="blob",
  211. fileName="/%s%s_rgb%s" % (layerCode, colorCode, self.imageExtenstion),
  212. categories=["%s%s" % (layerCode, colorCode)],
  213. mimeType=self.imageMimeType,
  214. )
  215. def writeData(self, mapper):
  216. width = self.renderWindow.GetSize()[0]
  217. height = self.renderWindow.GetSize()[1]
  218. if not self.depthToWrite:
  219. self.depthToWrite = bytearray(width * height)
  220. for cam in self.camera:
  221. self.updateCamera(cam)
  222. imagePath = self.dataHandler.getDataAbsoluteFilePath(self.activeRGBKey)
  223. depthPath = self.dataHandler.getDataAbsoluteFilePath(self.activeDepthKey)
  224. # -----------------------------------------------------------------
  225. # Write Image
  226. # -----------------------------------------------------------------
  227. mapper.GetColorImage(self.imageDataColor)
  228. self.imageWriter.SetFileName(imagePath)
  229. self.imageWriter.Write()
  230. # -----------------------------------------------------------------
  231. # Write Depth
  232. # -----------------------------------------------------------------
  233. mapper.GetDepthImage(self.imageDataDepth)
  234. inputArray = self.imageDataDepth.GetPointData().GetArray(0)
  235. size = inputArray.GetNumberOfTuples()
  236. for idx in range(size):
  237. self.depthToWrite[idx] = int(inputArray.GetValue(idx))
  238. with open(depthPath, "wb") as f:
  239. f.write(self.depthToWrite)
  240. def start(self, renderWindow, renderer):
  241. DataSetBuilder.start(self, renderWindow, renderer)
  242. self.camera.updatePriority([2, 1])
  243. def stop(self, compress=True):
  244. # Push metadata
  245. self.compositePipeline["dimensions"] = self.renderWindow.GetSize()
  246. self.compositePipeline["default_pipeline"] = (
  247. "A".join(self.compositePipeline["layers"]) + "A"
  248. )
  249. self.dataHandler.addSection("CompositePipeline", self.compositePipeline)
  250. # Write metadata
  251. DataSetBuilder.stop(self)
  252. if compress:
  253. for root, dirs, files in os.walk(self.dataHandler.getBasePath()):
  254. print("Compress", root)
  255. for name in files:
  256. if ".uint8" in name and ".gz" not in name:
  257. with open(os.path.join(root, name), "rb") as f_in:
  258. with gzip.open(
  259. os.path.join(root, name + ".gz"), "wb"
  260. ) as f_out:
  261. shutil.copyfileobj(f_in, f_out)
  262. os.remove(os.path.join(root, name))
  263. # -----------------------------------------------------------------------------
  264. # Data Prober Dataset Builder
  265. # -----------------------------------------------------------------------------
  266. class DataProberDataSetBuilder(DataSetBuilder):
  267. def __init__(
  268. self,
  269. location,
  270. sampling_dimesions,
  271. fields_to_keep,
  272. custom_probing_bounds=None,
  273. metadata={},
  274. ):
  275. DataSetBuilder.__init__(self, location, None, metadata)
  276. self.fieldsToWrite = fields_to_keep
  277. self.resamplerFilter = vtkPResampleFilter()
  278. self.resamplerFilter.SetSamplingDimension(sampling_dimesions)
  279. if custom_probing_bounds:
  280. self.resamplerFilter.SetUseInputBounds(0)
  281. self.resamplerFilter.SetCustomSamplingBounds(custom_probing_bounds)
  282. else:
  283. self.resamplerFilter.SetUseInputBounds(1)
  284. # Register all fields
  285. self.dataHandler.addTypes("data-prober", "binary")
  286. self.DataProber = {
  287. "types": {},
  288. "dimensions": sampling_dimesions,
  289. "ranges": {},
  290. "spacing": [1, 1, 1],
  291. }
  292. for field in self.fieldsToWrite:
  293. self.dataHandler.registerData(
  294. name=field, type="array", fileName="/%s.array" % field
  295. )
  296. def setDataToProbe(self, dataset):
  297. self.resamplerFilter.SetInputData(dataset)
  298. def setSourceToProbe(self, source):
  299. self.resamplerFilter.SetInputConnection(source.GetOutputPort())
  300. def writeData(self):
  301. self.resamplerFilter.Update()
  302. arrays = self.resamplerFilter.GetOutput().GetPointData()
  303. for field in self.fieldsToWrite:
  304. array = arrays.GetArray(field)
  305. if array:
  306. b = memoryview(array)
  307. with open(self.dataHandler.getDataAbsoluteFilePath(field), "wb") as f:
  308. f.write(b)
  309. self.DataProber["types"][field] = getJSArrayType(array)
  310. if field in self.DataProber["ranges"]:
  311. dataRange = array.GetRange()
  312. if dataRange[0] < self.DataProber["ranges"][field][0]:
  313. self.DataProber["ranges"][field][0] = dataRange[0]
  314. if dataRange[1] > self.DataProber["ranges"][field][1]:
  315. self.DataProber["ranges"][field][1] = dataRange[1]
  316. else:
  317. self.DataProber["ranges"][field] = [
  318. array.GetRange()[0],
  319. array.GetRange()[1],
  320. ]
  321. else:
  322. print("No array for", field)
  323. print(self.resamplerFilter.GetOutput())
  324. def stop(self, compress=True):
  325. # Push metadata
  326. self.dataHandler.addSection("DataProber", self.DataProber)
  327. # Write metadata
  328. DataSetBuilder.stop(self)
  329. if compress:
  330. for root, dirs, files in os.walk(self.dataHandler.getBasePath()):
  331. print("Compress", root)
  332. for name in files:
  333. if ".array" in name and ".gz" not in name:
  334. with open(os.path.join(root, name), "rb") as f_in:
  335. with gzip.open(
  336. os.path.join(root, name + ".gz"), "wb"
  337. ) as f_out:
  338. shutil.copyfileobj(f_in, f_out)
  339. os.remove(os.path.join(root, name))
  340. # -----------------------------------------------------------------------------
  341. # Sorted Composite Dataset Builder
  342. # -----------------------------------------------------------------------------
  343. class ConvertVolumeStackToSortedStack(object):
  344. def __init__(self, width, height):
  345. self.width = width
  346. self.height = height
  347. self.layers = 0
  348. def convert(self, directory):
  349. imagePaths = {}
  350. depthPaths = {}
  351. layerNames = []
  352. for fileName in os.listdir(directory):
  353. if "_rgb" in fileName or "_depth" in fileName:
  354. fileId = fileName.split("_")[0][0]
  355. if "_rgb" in fileName:
  356. imagePaths[fileId] = os.path.join(directory, fileName)
  357. else:
  358. layerNames.append(fileId)
  359. depthPaths[fileId] = os.path.join(directory, fileName)
  360. layerNames.sort()
  361. if len(layerNames) == 0:
  362. return
  363. # Load data in Memory
  364. depthArrays = []
  365. imageReader = vtkPNGReader()
  366. numberOfValues = self.width * self.height * len(layerNames)
  367. imageSize = self.width * self.height
  368. self.layers = len(layerNames)
  369. # Write all images as single memoryview
  370. opacity = vtkUnsignedCharArray()
  371. opacity.SetNumberOfComponents(1)
  372. opacity.SetNumberOfTuples(numberOfValues)
  373. intensity = vtkUnsignedCharArray()
  374. intensity.SetNumberOfComponents(1)
  375. intensity.SetNumberOfTuples(numberOfValues)
  376. for layer in range(self.layers):
  377. imageReader.SetFileName(imagePaths[layerNames[layer]])
  378. imageReader.Update()
  379. rgbaArray = imageReader.GetOutput().GetPointData().GetArray(0)
  380. for idx in range(imageSize):
  381. intensity.SetValue(
  382. (layer * imageSize) + idx, rgbaArray.GetValue(idx * 4)
  383. )
  384. opacity.SetValue(
  385. (layer * imageSize) + idx, rgbaArray.GetValue(idx * 4 + 3)
  386. )
  387. with open(depthPaths[layerNames[layer]], "rb") as depthFile:
  388. depthArrays.append(depthFile.read())
  389. # Apply pixel sorting
  390. destOrder = vtkUnsignedCharArray()
  391. destOrder.SetNumberOfComponents(1)
  392. destOrder.SetNumberOfTuples(numberOfValues)
  393. opacityOrder = vtkUnsignedCharArray()
  394. opacityOrder.SetNumberOfComponents(1)
  395. opacityOrder.SetNumberOfTuples(numberOfValues)
  396. intensityOrder = vtkUnsignedCharArray()
  397. intensityOrder.SetNumberOfComponents(1)
  398. intensityOrder.SetNumberOfTuples(numberOfValues)
  399. for pixelIdx in range(imageSize):
  400. depthStack = []
  401. for depthArray in depthArrays:
  402. depthStack.append((depthArray[pixelIdx], len(depthStack)))
  403. depthStack.sort(key=lambda tup: tup[0])
  404. for destLayerIdx in range(len(depthStack)):
  405. sourceLayerIdx = depthStack[destLayerIdx][1]
  406. # Copy Idx
  407. destOrder.SetValue(
  408. (imageSize * destLayerIdx) + pixelIdx, sourceLayerIdx
  409. )
  410. opacityOrder.SetValue(
  411. (imageSize * destLayerIdx) + pixelIdx,
  412. opacity.GetValue((imageSize * sourceLayerIdx) + pixelIdx),
  413. )
  414. intensityOrder.SetValue(
  415. (imageSize * destLayerIdx) + pixelIdx,
  416. intensity.GetValue((imageSize * sourceLayerIdx) + pixelIdx),
  417. )
  418. with open(os.path.join(directory, "alpha.uint8"), "wb") as f:
  419. f.write(memoryview(opacityOrder))
  420. with open(os.path.join(directory, "intensity.uint8"), "wb") as f:
  421. f.write(memoryview(intensityOrder))
  422. with open(os.path.join(directory, "order.uint8"), "wb") as f:
  423. f.write(memoryview(destOrder))
  424. class SortedCompositeDataSetBuilder(VolumeCompositeDataSetBuilder):
  425. def __init__(self, location, cameraInfo, metadata={}, sections={}):
  426. VolumeCompositeDataSetBuilder.__init__(
  427. self, location, "image/png", cameraInfo, metadata, sections
  428. )
  429. self.dataHandler.addTypes("sorted-composite", "rgba")
  430. # Register order and color textures
  431. self.layerScalars = []
  432. self.dataHandler.registerData(
  433. name="order", type="array", fileName="/order.uint8"
  434. )
  435. self.dataHandler.registerData(
  436. name="alpha", type="array", fileName="/alpha.uint8"
  437. )
  438. self.dataHandler.registerData(
  439. name="intensity",
  440. type="array",
  441. fileName="/intensity.uint8",
  442. categories=["intensity"],
  443. )
  444. def start(self, renderWindow, renderer):
  445. VolumeCompositeDataSetBuilder.start(self, renderWindow, renderer)
  446. imageSize = self.renderWindow.GetSize()
  447. self.dataConverter = ConvertVolumeStackToSortedStack(imageSize[0], imageSize[1])
  448. def activateLayer(self, colorBy, scalar):
  449. VolumeCompositeDataSetBuilder.activateLayer(
  450. self, "root", "%s" % scalar, colorBy
  451. )
  452. self.layerScalars.append(scalar)
  453. def writeData(self, mapper):
  454. VolumeCompositeDataSetBuilder.writeData(self, mapper)
  455. # Fill data pattern
  456. self.dataHandler.getDataAbsoluteFilePath("order")
  457. self.dataHandler.getDataAbsoluteFilePath("alpha")
  458. self.dataHandler.getDataAbsoluteFilePath("intensity")
  459. def stop(self, clean=True, compress=True):
  460. VolumeCompositeDataSetBuilder.stop(self, compress=False)
  461. # Go through all directories and convert them
  462. for root, dirs, files in os.walk(self.dataHandler.getBasePath()):
  463. for name in dirs:
  464. print("Process", os.path.join(root, name))
  465. self.dataConverter.convert(os.path.join(root, name))
  466. # Rename index.json to info_origin.json
  467. os.rename(
  468. os.path.join(self.dataHandler.getBasePath(), "index.json"),
  469. os.path.join(self.dataHandler.getBasePath(), "index_origin.json"),
  470. )
  471. # Update index.json
  472. with open(
  473. os.path.join(self.dataHandler.getBasePath(), "index_origin.json"), "r"
  474. ) as infoFile:
  475. metadata = json.load(infoFile)
  476. metadata["SortedComposite"] = {
  477. "dimensions": metadata["CompositePipeline"]["dimensions"],
  478. "layers": self.dataConverter.layers,
  479. "scalars": self.layerScalars[0 : self.dataConverter.layers],
  480. }
  481. # Clean metadata
  482. dataToKeep = []
  483. del metadata["CompositePipeline"]
  484. for item in metadata["data"]:
  485. if item["name"] in ["order", "alpha", "intensity"]:
  486. dataToKeep.append(item)
  487. metadata["data"] = dataToKeep
  488. metadata["type"] = ["tonic-query-data-model", "sorted-composite", "alpha"]
  489. # Override index.json
  490. with open(
  491. os.path.join(self.dataHandler.getBasePath(), "index.json"), "w"
  492. ) as newMetaFile:
  493. newMetaFile.write(json.dumps(metadata))
  494. # Clean temporary data
  495. if clean:
  496. for root, dirs, files in os.walk(self.dataHandler.getBasePath()):
  497. print("Clean", root)
  498. for name in files:
  499. if (
  500. "_rgb.png" in name
  501. or "_depth.uint8" in name
  502. or name == "index_origin.json"
  503. ):
  504. os.remove(os.path.join(root, name))
  505. if compress:
  506. for root, dirs, files in os.walk(self.dataHandler.getBasePath()):
  507. print("Compress", root)
  508. for name in files:
  509. if ".uint8" in name and ".gz" not in name:
  510. with open(os.path.join(root, name), "rb") as f_in:
  511. with gzip.open(
  512. os.path.join(root, name + ".gz"), "wb"
  513. ) as f_out:
  514. shutil.copyfileobj(f_in, f_out)
  515. os.remove(os.path.join(root, name))