head_reconstruction_pipeline.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608
  1. # Copyright (c) Alibaba, Inc. and its affiliates.
  2. import io
  3. import os
  4. import shutil
  5. from typing import Any, Dict
  6. import cv2
  7. import face_alignment
  8. import numpy as np
  9. import PIL.Image
  10. import tensorflow as tf
  11. import torch
  12. from scipy.io import loadmat, savemat
  13. from modelscope.metainfo import Pipelines
  14. from modelscope.models.cv.face_reconstruction.models.facelandmark.large_base_lmks_infer import \
  15. LargeBaseLmkInfer
  16. from modelscope.models.cv.face_reconstruction.utils import (
  17. POS, align_for_lm, draw_line, enlarged_bbox, extract_5p, image_warp_grid1,
  18. load_lm3d, mesh_to_string, read_obj, resize_n_crop_img,
  19. resize_on_long_side, spread_flow, write_obj)
  20. from modelscope.models.cv.head_reconstruction.models.head_segmentation import \
  21. HeadSegmentor
  22. from modelscope.models.cv.head_reconstruction.models.tex_processor import \
  23. TexProcesser
  24. from modelscope.models.cv.skin_retouching.retinaface.predict_single import \
  25. Model
  26. from modelscope.outputs import OutputKeys
  27. from modelscope.pipelines import pipeline
  28. from modelscope.pipelines.base import Input, Pipeline
  29. from modelscope.pipelines.builder import PIPELINES
  30. from modelscope.preprocessors import LoadImage
  31. from modelscope.utils.config import Config
  32. from modelscope.utils.constant import ModelFile, Tasks
  33. from modelscope.utils.device import create_device, device_placement
  34. from modelscope.utils.logger import get_logger
  35. try:
  36. from torch.hub import get_dir
  37. except BaseException:
  38. from torch.hub import _get_torch_home as get_dir
  39. if tf.__version__ >= '2.0':
  40. tf = tf.compat.v1
  41. tf.disable_eager_execution()
  42. logger = get_logger()
  43. @PIPELINES.register_module(
  44. Tasks.head_reconstruction, module_name=Pipelines.head_reconstruction)
  45. class HeadReconstructionPipeline(Pipeline):
  46. def __init__(self, model: str, device: str, hair_tex=False):
  47. """The inference pipeline for head reconstruction task.
  48. Args:
  49. model (`str` or `Model` or module instance): A model instance or a model local dir
  50. or a model id in the model hub.
  51. device ('str'): device str, should be either cpu, cuda, gpu, gpu:X or cuda:X.
  52. Example:
  53. >>> from modelscope.pipelines import pipeline
  54. >>> test_image = 'data/test/images/face_reconstruction.jpg'
  55. >>> pipeline_headRecon = pipeline('head-reconstruction',
  56. model='damo/cv_HRN_head-reconstruction')
  57. >>> result = pipeline_headRecon(test_image)
  58. >>> mesh = result[OutputKeys.OUTPUT]['mesh']
  59. >>> texture_map = result[OutputKeys.OUTPUT_IMG]
  60. >>> mesh['texture_map'] = texture_map
  61. >>> write_obj('head_reconstruction.obj', mesh)
  62. """
  63. super().__init__(model=model, device=device)
  64. model_root = model
  65. bfm_folder = os.path.join(model_root, 'assets')
  66. checkpoint_path = os.path.join(model_root, ModelFile.TORCH_MODEL_FILE)
  67. config_path = os.path.join(model_root, ModelFile.CONFIGURATION)
  68. logger.info(f'loading config from {config_path}')
  69. self.cfg = Config.from_file(config_path)
  70. self.hair_tex = hair_tex
  71. if 'gpu' in device:
  72. self.device_name_ = 'cuda'
  73. else:
  74. self.device_name_ = device
  75. self.device_name_ = self.device_name_.lower()
  76. lmks_cpkt_path = os.path.join(model_root, 'large_base_net.pth')
  77. self.large_base_lmks_model = LargeBaseLmkInfer.model_preload(
  78. lmks_cpkt_path, self.device_name_ == 'cuda')
  79. self.detector = Model(max_size=512, device=self.device_name_)
  80. detector_ckpt_name = 'retinaface_resnet50_2020-07-20_old_torch.pth'
  81. state_dict = torch.load(
  82. os.path.join(os.path.dirname(lmks_cpkt_path), detector_ckpt_name),
  83. map_location='cpu',
  84. weights_only=True)
  85. self.detector.load_state_dict(state_dict)
  86. self.detector.eval()
  87. device = torch.device(self.device_name_)
  88. self.model.set_device(device)
  89. self.model.setup(checkpoint_path)
  90. self.model.parallelize()
  91. self.model.eval()
  92. self.model.set_render()
  93. hub_dir = get_dir()
  94. save_ckpt_dir = os.path.join(hub_dir, 'checkpoints')
  95. if not os.path.exists(save_ckpt_dir):
  96. os.makedirs(save_ckpt_dir)
  97. shutil.copy(
  98. os.path.join(model_root, 'face_alignment', 's3fd-619a316812.pth'),
  99. save_ckpt_dir)
  100. shutil.copy(
  101. os.path.join(model_root, 'face_alignment',
  102. '3DFAN4-4a694010b9.zip'), save_ckpt_dir)
  103. shutil.copy(
  104. os.path.join(model_root, 'face_alignment', 'depth-6c4283c0e0.zip'),
  105. save_ckpt_dir)
  106. self.lm_sess = face_alignment.FaceAlignment(
  107. face_alignment.LandmarksType.THREE_D,
  108. flip_input=False) # face_alignment.LandmarksType._3D
  109. config = tf.ConfigProto(allow_soft_placement=True)
  110. config.gpu_options.per_process_gpu_memory_fraction = 0.2
  111. config.gpu_options.allow_growth = True
  112. g1 = tf.Graph()
  113. self.face_sess = tf.Session(graph=g1, config=config)
  114. with self.face_sess.as_default():
  115. with g1.as_default():
  116. with tf.gfile.FastGFile(
  117. os.path.join(model_root, 'segment_face.pb'),
  118. 'rb') as f:
  119. graph_def = tf.GraphDef()
  120. graph_def.ParseFromString(f.read())
  121. self.face_sess.graph.as_default()
  122. tf.import_graph_def(graph_def, name='')
  123. self.face_sess.run(tf.global_variables_initializer())
  124. self.head_segmentor = HeadSegmentor(model_root=model_root)
  125. self.tex_processor = TexProcesser(model_root=model_root)
  126. self.lm3d_std = load_lm3d(bfm_folder)
  127. self.align_params = loadmat(
  128. '{}/assets/BBRegressorParam_r.mat'.format(model_root))
  129. device = create_device(self.device_name)
  130. self.device = device
  131. def preprocess(self, input: Input) -> Dict[str, Any]:
  132. if isinstance(input, str):
  133. img = LoadImage.convert_to_ndarray(input)
  134. if len(img.shape) == 2:
  135. img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
  136. img = img.astype(float)
  137. else:
  138. img = input.astype(float)
  139. result = {'img': img}
  140. return result
  141. def align_img(self,
  142. img,
  143. lm,
  144. lm3D,
  145. mask=None,
  146. target_size=224.,
  147. rescale_factor=102.):
  148. """
  149. Return:
  150. transparams --numpy.array (raw_W, raw_H, scale, tx, ty)
  151. img_new --PIL.Image (target_size, target_size, 3)
  152. lm_new --numpy.array (68, 2), y direction is opposite to v direction
  153. mask_new --PIL.Image (target_size, target_size)
  154. Parameters:
  155. img --PIL.Image (raw_H, raw_W, 3)
  156. lm --numpy.array (68, 2), y direction is opposite to v direction
  157. lm3D --numpy.array (5, 3)
  158. mask --PIL.Image (raw_H, raw_W, 3)
  159. """
  160. w0, h0 = img.size
  161. if lm.shape[0] != 5:
  162. lm5p = extract_5p(lm)
  163. else:
  164. lm5p = lm
  165. # calculate translation and scale factors using 5 facial landmarks and standard landmarks of a 3D face
  166. t, s = POS(lm5p.transpose(), lm3D.transpose())
  167. s = rescale_factor / s
  168. # processing the image
  169. img_new, lm_new, mask_new = resize_n_crop_img(
  170. img, lm, t, s, target_size=target_size, mask=mask)
  171. trans_params = np.array([w0, h0, s, t[0][0], t[1][0]])
  172. return trans_params, img_new, lm_new, mask_new
  173. def read_data(self,
  174. img,
  175. lm,
  176. lm3d_std,
  177. to_tensor=True,
  178. image_res=1024,
  179. img_fat=None,
  180. head_mask=None,
  181. rescale_factor=75.0):
  182. # to RGB
  183. im = PIL.Image.fromarray(img[..., ::-1])
  184. W, H = im.size
  185. lm[:, -1] = H - 1 - lm[:, -1]
  186. head_mask = PIL.Image.fromarray(head_mask)
  187. im_fat = PIL.Image.fromarray(img_fat[..., ::-1])
  188. _, im_lr_coeff, lm_lr_coeff, _ = self.align_img(im, lm, lm3d_std)
  189. _, im_lr, lm_lr, mask_lr_head = self.align_img(
  190. im, lm, lm3d_std, mask=head_mask, rescale_factor=rescale_factor)
  191. _, im_hd, lm_hd, _ = self.align_img(
  192. im_fat,
  193. lm,
  194. lm3d_std,
  195. target_size=image_res,
  196. rescale_factor=rescale_factor * image_res / 224)
  197. mask_lr = self.face_sess.run(
  198. self.face_sess.graph.get_tensor_by_name('output_alpha:0'),
  199. feed_dict={'input_image:0': np.array(im_lr)})
  200. if to_tensor:
  201. im_lr = torch.tensor(
  202. np.array(im_lr) / 255.,
  203. dtype=torch.float32).permute(2, 0, 1).unsqueeze(0)
  204. im_hd = torch.tensor(
  205. np.array(im_hd) / 255.,
  206. dtype=torch.float32).permute(2, 0, 1).unsqueeze(0)
  207. mask_lr = torch.tensor(
  208. np.array(mask_lr) / 255., dtype=torch.float32)[None,
  209. None, :, :]
  210. mask_lr_head = torch.tensor(
  211. np.array(mask_lr_head) / 255., dtype=torch.float32)[
  212. None, None, :, :] if mask_lr_head is not None else None
  213. lm_lr = torch.tensor(lm_lr).unsqueeze(0)
  214. lm_hd = torch.tensor(lm_hd).unsqueeze(0)
  215. im_lr_coeff = torch.tensor(
  216. np.array(im_lr_coeff) / 255.,
  217. dtype=torch.float32).permute(2, 0, 1).unsqueeze(0)
  218. lm_lr_coeff = torch.tensor(lm_lr_coeff).unsqueeze(0)
  219. return im_lr, lm_lr, im_hd, lm_hd, mask_lr, mask_lr_head, im_lr_coeff, lm_lr_coeff
  220. def prepare_data(self, img, lm_sess, five_points=None):
  221. input_img, scale, bbox = align_for_lm(
  222. img, five_points,
  223. self.align_params) # align for 68 landmark detection
  224. if scale == 0:
  225. return None
  226. # detect landmarks
  227. input_img = np.reshape(input_img, [1, 224, 224, 3]).astype(np.float32)
  228. input_img = input_img[0, :, :, ::-1]
  229. landmark = lm_sess.get_landmarks_from_image(input_img)[0]
  230. landmark = landmark[:, :2] / scale
  231. landmark[:, 0] = landmark[:, 0] + bbox[0]
  232. landmark[:, 1] = landmark[:, 1] + bbox[1]
  233. return landmark
  234. def infer_lmks(self, img_bgr):
  235. INPUT_SIZE = 224
  236. ENLARGE_RATIO = 1.35
  237. landmarks = []
  238. rgb_image = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)
  239. results = self.detector.predict_jsons(rgb_image)
  240. boxes = []
  241. for anno in results:
  242. if anno['score'] == -1:
  243. break
  244. boxes.append({
  245. 'x1': anno['bbox'][0],
  246. 'y1': anno['bbox'][1],
  247. 'x2': anno['bbox'][2],
  248. 'y2': anno['bbox'][3]
  249. })
  250. for detect_result in boxes:
  251. x1 = detect_result['x1']
  252. y1 = detect_result['y1']
  253. x2 = detect_result['x2']
  254. y2 = detect_result['y2']
  255. w = x2 - x1 + 1
  256. h = y2 - y1 + 1
  257. cx = (x2 + x1) / 2
  258. cy = (y2 + y1) / 2
  259. sz = max(h, w) * ENLARGE_RATIO
  260. x1 = cx - sz / 2
  261. y1 = cy - sz / 2
  262. trans_x1 = x1
  263. trans_y1 = y1
  264. x2 = x1 + sz
  265. y2 = y1 + sz
  266. height, width, _ = rgb_image.shape
  267. dx = max(0, -x1)
  268. dy = max(0, -y1)
  269. x1 = max(0, x1)
  270. y1 = max(0, y1)
  271. edx = max(0, x2 - width)
  272. edy = max(0, y2 - height)
  273. x2 = min(width, x2)
  274. y2 = min(height, y2)
  275. crop_img = rgb_image[int(y1):int(y2), int(x1):int(x2)]
  276. if dx > 0 or dy > 0 or edx > 0 or edy > 0:
  277. crop_img = cv2.copyMakeBorder(
  278. crop_img,
  279. int(dy),
  280. int(edy),
  281. int(dx),
  282. int(edx),
  283. cv2.BORDER_CONSTANT,
  284. value=(103.94, 116.78, 123.68))
  285. crop_img = cv2.resize(crop_img, (INPUT_SIZE, INPUT_SIZE))
  286. base_lmks = LargeBaseLmkInfer.infer_img(
  287. crop_img, self.large_base_lmks_model,
  288. self.device_name_ == 'cuda')
  289. inv_scale = sz / INPUT_SIZE
  290. affine_base_lmks = np.zeros((106, 2))
  291. for idx in range(106):
  292. affine_base_lmks[idx][
  293. 0] = base_lmks[0][idx * 2 + 0] * inv_scale + trans_x1
  294. affine_base_lmks[idx][
  295. 1] = base_lmks[0][idx * 2 + 1] * inv_scale + trans_y1
  296. x1 = np.min(affine_base_lmks[:, 0])
  297. y1 = np.min(affine_base_lmks[:, 1])
  298. x2 = np.max(affine_base_lmks[:, 0])
  299. y2 = np.max(affine_base_lmks[:, 1])
  300. w = x2 - x1 + 1
  301. h = y2 - y1 + 1
  302. cx = (x2 + x1) / 2
  303. cy = (y2 + y1) / 2
  304. sz = max(h, w) * ENLARGE_RATIO
  305. x1 = cx - sz / 2
  306. y1 = cy - sz / 2
  307. trans_x1 = x1
  308. trans_y1 = y1
  309. x2 = x1 + sz
  310. y2 = y1 + sz
  311. height, width, _ = rgb_image.shape
  312. dx = max(0, -x1)
  313. dy = max(0, -y1)
  314. x1 = max(0, x1)
  315. y1 = max(0, y1)
  316. edx = max(0, x2 - width)
  317. edy = max(0, y2 - height)
  318. x2 = min(width, x2)
  319. y2 = min(height, y2)
  320. crop_img = rgb_image[int(y1):int(y2), int(x1):int(x2)]
  321. if dx > 0 or dy > 0 or edx > 0 or edy > 0:
  322. crop_img = cv2.copyMakeBorder(
  323. crop_img,
  324. int(dy),
  325. int(edy),
  326. int(dx),
  327. int(edx),
  328. cv2.BORDER_CONSTANT,
  329. value=(103.94, 116.78, 123.68))
  330. crop_img = cv2.resize(crop_img, (INPUT_SIZE, INPUT_SIZE))
  331. base_lmks = LargeBaseLmkInfer.infer_img(
  332. crop_img, self.large_base_lmks_model,
  333. self.device_name_.lower() == 'cuda')
  334. inv_scale = sz / INPUT_SIZE
  335. affine_base_lmks = np.zeros((106, 2))
  336. for idx in range(106):
  337. affine_base_lmks[idx][
  338. 0] = base_lmks[0][idx * 2 + 0] * inv_scale + trans_x1
  339. affine_base_lmks[idx][
  340. 1] = base_lmks[0][idx * 2 + 1] * inv_scale + trans_y1
  341. landmarks.append(affine_base_lmks)
  342. return boxes, landmarks
  343. def find_face_contour(self, image):
  344. boxes, landmarks = self.infer_lmks(image)
  345. landmarks = np.array(landmarks)
  346. args = [[0, 33, False], [33, 38, False], [42, 47, False],
  347. [51, 55, False], [57, 64, False], [66, 74, True],
  348. [75, 83, True], [84, 96, True]]
  349. roi_bboxs = []
  350. for i in range(len(boxes)):
  351. roi_bbox = enlarged_bbox([
  352. boxes[i]['x1'], boxes[i]['y1'], boxes[i]['x2'], boxes[i]['y2']
  353. ], image.shape[1], image.shape[0], 0.5)
  354. roi_bbox = [int(x) for x in roi_bbox]
  355. roi_bboxs.append(roi_bbox)
  356. people_maps = []
  357. for i in range(landmarks.shape[0]):
  358. landmark = landmarks[i, :, :]
  359. maps = []
  360. whole_mask = np.zeros((image.shape[0], image.shape[1]), np.uint8)
  361. roi_box = roi_bboxs[i]
  362. roi_box_width = roi_box[2] - roi_box[0]
  363. roi_box_height = roi_box[3] - roi_box[1]
  364. short_side_length = roi_box_width if roi_box_width < roi_box_height else roi_box_height
  365. line_width = short_side_length // 10
  366. if line_width == 0:
  367. line_width = 1
  368. kernel_size = line_width * 2
  369. gaussian_kernel = kernel_size if kernel_size % 2 == 1 else kernel_size + 1
  370. for t, arg in enumerate(args):
  371. mask = np.zeros((image.shape[0], image.shape[1]), np.uint8)
  372. draw_line(mask, landmark[arg[0]:arg[1]], (255, 255, 255),
  373. line_width, arg[2])
  374. mask = cv2.GaussianBlur(mask,
  375. (gaussian_kernel, gaussian_kernel), 0)
  376. if t >= 1:
  377. draw_line(whole_mask, landmark[arg[0]:arg[1]],
  378. (255, 255, 255), line_width * 2, arg[2])
  379. maps.append(mask)
  380. whole_mask = cv2.GaussianBlur(whole_mask,
  381. (gaussian_kernel, gaussian_kernel),
  382. 0)
  383. maps.append(whole_mask)
  384. people_maps.append(maps)
  385. return people_maps[0], boxes
  386. def fat_face(self, img, degree=0.04):
  387. _img, scale = resize_on_long_side(img, 800)
  388. contour_maps, boxes = self.find_face_contour(_img)
  389. contour_map = contour_maps[0]
  390. boxes = boxes[0]
  391. Flow = np.zeros(
  392. shape=(contour_map.shape[0], contour_map.shape[1], 2),
  393. dtype=np.float32)
  394. box_center = [(boxes['x1'] + boxes['x2']) / 2,
  395. (boxes['y1'] + boxes['y2']) / 2]
  396. box_length = max(
  397. abs(boxes['y1'] - boxes['y2']), abs(boxes['x1'] - boxes['x2']))
  398. value_1 = 2 * (Flow.shape[0] - box_center[1] - 1)
  399. value_2 = 2 * (Flow.shape[1] - box_center[0] - 1)
  400. value_list = [
  401. box_length * 2, 2 * (box_center[0] - 1), 2 * (box_center[1] - 1),
  402. value_1, value_2
  403. ]
  404. flow_box_length = min(value_list)
  405. flow_box_length = int(flow_box_length)
  406. sf = spread_flow(100, flow_box_length * degree)
  407. sf = cv2.resize(sf, (flow_box_length, flow_box_length))
  408. Flow[int(box_center[1]
  409. - flow_box_length / 2):int(box_center[1]
  410. + flow_box_length / 2),
  411. int(box_center[0]
  412. - flow_box_length / 2):int(box_center[0]
  413. + flow_box_length / 2)] = sf
  414. Flow = Flow * np.dstack((contour_map, contour_map)) / 255.0
  415. inter_face_maps = contour_maps[-1]
  416. Flow = Flow * (1.0 - np.dstack(
  417. (inter_face_maps, inter_face_maps)) / 255.0)
  418. Flow = cv2.resize(Flow, (img.shape[1], img.shape[0]))
  419. Flow = Flow / scale
  420. pred, top_bound, bottom_bound, left_bound, right_bound = image_warp_grid1(
  421. Flow[..., 0], Flow[..., 1], img, 1.0, [0, 0, 0, 0])
  422. return pred
  423. def forward(self, input: Dict[str, Any]) -> Dict[str, Any]:
  424. rgb_image = input['img'].cpu().numpy().astype(np.uint8)
  425. bgr_image = cv2.cvtColor(rgb_image, cv2.COLOR_RGB2BGR)
  426. img = bgr_image
  427. if img.shape[0] > 2000 or img.shape[1] > 2000:
  428. img, _ = resize_on_long_side(img, 1500)
  429. box, results = self.infer_lmks(img)
  430. if results is None or np.array(results).shape[0] == 0:
  431. return {}
  432. fatbgr = self.fat_face(img)
  433. landmarks = []
  434. results = results[0]
  435. for idx in [74, 83, 54, 84, 90]:
  436. landmarks.append([results[idx][0], results[idx][1]])
  437. landmarks = np.array(landmarks)
  438. landmarks = self.prepare_data(img, self.lm_sess, five_points=landmarks)
  439. head_mask = self.head_segmentor.process(img)[0]
  440. im_tensor, lm_tensor, im_hd_tensor, lm_hd_tensor, mask, head_mask, im_co, lm_co = self.read_data(
  441. img, landmarks, self.lm3d_std, img_fat=fatbgr, head_mask=head_mask)
  442. data = {
  443. 'imgs': im_tensor,
  444. 'imgs_hd': im_hd_tensor,
  445. 'lms': lm_tensor,
  446. 'lms_hd': lm_hd_tensor,
  447. 'face_mask': mask,
  448. 'head_mask': head_mask,
  449. 'imgs_coeff': im_co,
  450. 'lms_coeff': lm_co,
  451. }
  452. self.model.set_input(data) # unpack data from data loader
  453. output = self.model() # run inference
  454. assert output is not None
  455. tex_map = output['tex_map'].astype(np.float32)
  456. # post-process texture map
  457. tex_map = self.tex_processor.post_process_texture(
  458. tex_map, hair_tex=self.hair_tex)
  459. head_mesh = {
  460. 'vertices': output['vertices'],
  461. 'faces': output['triangles'] + 1,
  462. 'UVs': output['uvs'],
  463. 'faces_uv': output['faces_uv'],
  464. 'normals': output['normals'],
  465. 'texture_map': tex_map
  466. }
  467. results = {
  468. 'mesh': head_mesh,
  469. }
  470. return {
  471. OutputKeys.OUTPUT_OBJ: None,
  472. OutputKeys.OUTPUT_IMG: tex_map,
  473. OutputKeys.OUTPUT: results
  474. }
  475. def postprocess(self, inputs, **kwargs) -> Dict[str, Any]:
  476. render = kwargs.get('render', False)
  477. output_obj = inputs[OutputKeys.OUTPUT_OBJ]
  478. texture_map = inputs[OutputKeys.OUTPUT_IMG]
  479. results = inputs[OutputKeys.OUTPUT]
  480. if render:
  481. output_obj = io.BytesIO()
  482. mesh_str = mesh_to_string(results['mesh'])
  483. mesh_bytes = mesh_str.encode(encoding='utf-8')
  484. output_obj.write(mesh_bytes)
  485. result = {
  486. OutputKeys.OUTPUT_OBJ: output_obj,
  487. OutputKeys.OUTPUT_IMG: texture_map,
  488. OutputKeys.OUTPUT: None if render else results,
  489. }
  490. return result