code: def save_obj(
      f: PathOrStr,
      verts,
      faces,
      decimal_places: Optional[int] = None,
      path_manager: Optional[PathManager] = None,
      *,
      normals: Optional[torch.Tensor] = None,
      faces_normals_idx: Optional[torch.Tensor] = None,
      verts_uvs: Optional[torch.Tensor] = None,
      faces_uvs: Optional[torch.Tensor] = None,
      texture_map: Optional[torch.Tensor] = None,
  ) -> None:
      """
      Save a mesh to an .obj file.
  
      Args:
          f: File (str or path) to which the mesh should be written.
          verts: FloatTensor of shape (V, 3) giving vertex coordinates.
          faces: LongTensor of shape (F, 3) giving faces.
          decimal_places: Number of decimal places for saving.
          path_manager: Optional PathManager for interpreting f if
              it is a str.
          normals: FloatTensor of shape (V, 3) giving normals for faces_normals_idx
              to index into.
          faces_normals_idx: LongTensor of shape (F, 3) giving the index into
              normals for each vertex in the face.
          verts_uvs: FloatTensor of shape (V, 2) giving the uv coordinate per vertex.
          faces_uvs: LongTensor of shape (F, 3) giving the index into verts_uvs for
              each vertex in the face.
          texture_map: FloatTensor of shape (H, W, 3) representing the texture map
              for the mesh which will be saved as an image. The values are expected
              to be in the range [0, 1],
      """
      if len(verts) and (verts.dim() != 2 or verts.size(1) != 3):
          message = "'verts' should either be empty or of shape (num_verts, 3)."
          raise ValueError(message)
  
      if len(faces) and (faces.dim() != 2 or faces.size(1) != 3):
          message = "'faces' should either be empty or of shape (num_faces, 3)."
          raise ValueError(message)
  
      if (normals is None) != (faces_normals_idx is None):
          message = "'normals' and 'faces_normals_idx' must both be None or neither."
          raise ValueError(message)
  
      if faces_normals_idx is not None and (
          faces_normals_idx.dim() != 2 or faces_normals_idx.size(1) != 3
      ):
          message = "'faces_normals_idx' should either be empty or of shape (num_faces, 3)."
          raise ValueError(message)
  
      if normals is not None and (
          normals.dim() != 2 or normals.size(1) != 3
      ):
          message = "'normals' should either be empty or of shape (num_verts, 3)."
          raise ValueError(message)
  
      if faces_uvs is not None and (
          faces_uvs.dim() != 2 or faces_uvs.size(1) != 3
      ):
          message = "'faces_uvs' should either be empty or of shape (num_faces, 3)."
          raise ValueError(message)
  
      if verts_uvs is not None and (
          verts_uvs.dim() != 2 or verts_uvs.size(1) != 2
      ):
          message = "'verts_uvs' should either be empty or of shape (num_verts, 2)."
          raise ValueError(message)
  
      if texture_map is not None and (
          texture_map.dim() != 3 or texture_map.size(2) != 3
      ):
          message = "'texture_map' should either be empty or of shape (H, W, 3)."
          raise ValueError(message)
  
      if path_manager is None:
          path_manager = PathManager()
  
      save_texture = all(
          t is not None for t in [faces_uvs, verts_uvs, texture_map]
      )
      output_path = Path(f)
  
      # Save the .obj file
      with _open_file(f, path_manager, "w") as f:
          if save_texture:
              # Add the header required for the texture info to be loaded correctly
              obj_header = "\nmtllib {0}.mtl\nusemtl mesh\n\n".format(
                  output_path.stem
              )
              f.write(obj_header)
          _save(
              f,
              verts,
              faces,
              decimal_places,
              normals=normals,
              faces_normals_idx=faces_normals_idx,
              verts_uvs=verts_uvs,
              faces_uvs=faces_uvs,
              save_texture=save_texture,
              save_normals=normals is not None,
          )
  
      # Save the .mtl and .png files associated with the texture
      if save_texture:
          image_path = output_path.with_suffix(".png")
          mtl_path = output_path.with_suffix(".mtl")
          if isinstance(f, str):
              # Back to str for iopath interpretation.
              image_path = str(image_path)
              mtl_path = str(mtl_path)
  
          # Save texture map to output folder
          # pyre-fixme[16] # undefined attribute cpu
          texture_map = texture_map.detach().cpu() * 255.0
          image = Image.fromarray(texture_map.numpy().astype(np.uint8))
          with _open_file(image_path, path_manager, "wb") as im_f:
              image.save(im_f)
  
          # Create .mtl file with the material name and texture map filename
          # TODO: enable material properties to also be saved.
          with _open_file(mtl_path, path_manager, "w") as f_mtl:
              lines = (
                  f"newmtl mesh\n" f"map_Kd {output_path.stem}.png\n"
              )
              f_mtl.write(lines)
  
imports: import os
  import warnings
  from collections import namedtuple
  from pathlib import Path
  from typing import List, Optional
  import numpy
  import torch
  from iopath.common.file_io import PathManager
  from PIL import Image
  from pytorch3d.common.datatypes import Device
  from pytorch3d.io.mtl_io import load_mtl, make_mesh_texture_atlas
  from pytorch3d.io.utils import _check_faces_indices, _make_tensor, _open_file, PathOrStr
  from pytorch3d.renderer import TexturesAtlas, TexturesUV
  from pytorch3d.structures import join_meshes_as_batch, Meshes
  from .pluggable_formats import endswith, MeshFormatInterpreter
label_desc: Use image node_id 8 for calling a global function with example usage: save_obj(f, verts, faces, decimal_places, path_manager) without return types