Genesis icon indicating copy to clipboard operation
Genesis copied to clipboard

BUG. Can not visualize texture.

Open Riften opened this issue 1 year ago • 1 comments

Genesis can not visualize texture for the MJCF model attached.

import genesis as gs
import os
gs.init(backend=gs.cuda)
scene = gs.Scene(show_viewer=True)
mjcf_morph = gs.morphs.MJCF(file=os.path.join('texture_bug', 'floor_room.xml'))
scene.add_entity(mjcf_morph)
scene.build()

Genesis result:

genesis

Mujoco (3.2.5) result:

mujoco

MJCF file with texture image:

texture_bug.zip

MJCF file

<mujoco model="base">
  <option impratio="20" cone="elliptic" density="1.2" viscosity="0.00002" timestep="0.002" />
<compiler angle="radian" meshdir="meshes/" inertiagrouprange="0 0" autolimits="true" />
<size nconmax="5000" njmax="5000" />
<asset>
    <material texrepeat="2 2" texuniform="true" reflectance="0.1" shininess="0.1" name="floor_room_wall_mat" texture="floor_room_wall" />
  <texture type="2d" name="floor_room_wall" file="light_wood_planks_long.png" />
  </asset>
<worldbody>
    <body pos="2.15 -2.5 -0.02" name="floor_room_main" quat="0.707 0 0 0.707">
      
    <geom conaffinity="0" contype="0" mass="100" material="floor_room_wall_mat" type="box" size="2.54 3.19 0.02" pos="0 0 0" group="1" name="floor_room_g0_vis" />
    <site type="sphere" group="0" pos="0 0 0" size="0.002 0.002 0.002" rgba="1 0 0 1" name="floor_room_default_site" />
    </body>
  </worldbody>
</mujoco>

Riften avatar Dec 22 '24 13:12 Riften

maybe try this example in rendering folder - see if it imports texture like this ? Screencast from 2024-12-22 12-35-40.webm

OIEIEIO avatar Dec 22 '24 17:12 OIEIEIO

maybe try this example in rendering folder - see if it imports texture like this ? Screencast from 2024-12-22 12-35-40.webm

I got the same rendering result as you. The texture is imported fine.

Riften avatar Dec 23 '24 04:12 Riften

I notice that the texture is rendered fine when it is defined within .obj mesh file. But the texture defined through <texture> element within MJCF file is not rendered properly. Here is the rendering result of a kitchen scene from robocasa. There is ValueError: Incorrect texture coordinate shape and ValueError: Cannot reformat 2-channel texture into RGBA error raised for some objects. I have removed them from scene.

https://github.com/user-attachments/assets/ce67c0ae-96ae-4509-80ad-c0df7c17832b

There are some objects rendered with texture.

The result from mujoco 3.2.5 for the same scene.

mujoco_whole

Riften avatar Dec 23 '24 04:12 Riften

@zhouxian 抱歉打扰,请问上面提到的问题是 genesis 对 mjcf 的支持问题,还是我对 texture 的导入方式不对?后续有没有修复或特性支持的计划,谢谢。

Riften avatar Dec 23 '24 12:12 Riften

抱歉我们暂时人手有些不够issue有点多😅一定会修复的! 我们会尽可能支持各种mesh和texture格式。(现在对mjcf的支持应该已经比isaac gym好很多了吧😬

如果过两周还没修复再戳我们一下,感谢!

On Mon, Dec 23, 2024 at 8:35 PM Riften @.***> wrote:

@zhouxian https://github.com/zhouxian 抱歉打扰,请问上面提到的问题是 genesis 对 mjcf 的支持问题,还是我对 texture 的导入方式不对?后续有没有修复或特性支持的计划,谢谢。

— Reply to this email directly, view it on GitHub https://github.com/Genesis-Embodied-AI/Genesis/issues/236#issuecomment-2559627661, or unsubscribe https://github.com/notifications/unsubscribe-auth/AEV4V6IKIPBR6FJ3DTPBZ2D2G77QBAVCNFSM6AAAAABUBU4LLWVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDKNJZGYZDONRWGE . You are receiving this because you were mentioned.Message ID: @.***>

zhouxian avatar Dec 23 '24 12:12 zhouxian

In #288, box texture is loaded inside mjcf parser. At least the wooden plane looks good. Please give it a try and let me know if it works. If so, I will merge it into the main branch.

BTW, @Riften what's the easiest way to get a few mjcf files of robocasa kitchen environment? I want to load it to see if there is anything missing in our mjcf parser

zhenjia-xu avatar Dec 24 '24 10:12 zhenjia-xu

In #288, box texture is loaded inside mjcf parser. At least the wooden plane looks good. Please give it a try and let me know if it works. If so, I will merge it into the main branch.

BTW, @Riften what's the easiest way to get a few mjcf files of robocasa kitchen environment? I want to load it to see if there is anything missing in our mjcf parser

The floor texture seems to be fine.

floor_fix_2

But there is still some error when load some other texture. The mjcf file which raises error is attached.

texture_bug_2.zip

Full error message
{
	"name": "TypeError",
	"message": "Cannot handle this data type: (1, 1, 600), |u1",
	"stack": "---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
File ~/App/anaconda3/envs/robotic/lib/python3.9/site-packages/PIL/Image.py:3098, in fromarray(obj, mode)
   3097 try:
-> 3098     mode, rawmode = _fromarray_typemap[typekey]
   3099 except KeyError as e:

KeyError: ((1, 1, 600), '|u1')

The above exception was the direct cause of the following exception:

TypeError                                 Traceback (most recent call last)
Cell In[5], line 2
      1 #mjcf_entity = scene.add_entity(mjcf_morph, surface=file_surface)
----> 2 mjcf_entity = scene.add_entity(mjcf_morph)
      3 #obj_entity = scene.add_entity(obj_morph)

File ~/App/anaconda3/envs/robotic/lib/python3.9/site-packages/genesis/utils/misc.py:38, in assert_unbuilt.<locals>.wrapper(self, *args, **kwargs)
     36 if self.is_built:
     37     gs.raise_exception(\"Scene is already built.\")
---> 38 return method(self, *args, **kwargs)

File ~/App/anaconda3/envs/robotic/lib/python3.9/site-packages/genesis/engine/scene.py:360, in Scene.add_entity(self, morph, material, surface, visualize_contact, vis_mode)
    357     else:
    358         morph.decompose_nonconvex = False
--> 360 entity = self._sim._add_entity(morph, material, surface, visualize_contact)
    362 return entity

File ~/App/anaconda3/envs/robotic/lib/python3.9/site-packages/genesis/engine/simulator.py:134, in Simulator._add_entity(self, morph, material, surface, visualize_contact)
    131     entity = self.avatar_solver.add_entity(self.n_entities, material, morph, surface, visualize_contact)
    133 elif isinstance(material, gs.materials.Rigid):
--> 134     entity = self.rigid_solver.add_entity(self.n_entities, material, morph, surface, visualize_contact)
    136 elif isinstance(material, gs.materials.MPM.Base):
    137     entity = self.mpm_solver.add_entity(self.n_entities, material, morph, surface)

File ~/App/anaconda3/envs/robotic/lib/python3.9/site-packages/genesis/engine/solvers/rigid/rigid_solver_decomp.py:63, in RigidSolver.add_entity(self, idx, material, morph, surface, visualize_contact)
     60     else:
     61         entity_class = RigidEntity
---> 63 entity = entity_class(
     64     scene=self._scene,
     65     solver=self,
     66     material=material,
     67     morph=morph,
     68     surface=surface,
     69     idx=idx,
     70     idx_in_solver=self.n_entities,
     71     link_start=self.n_links,
     72     joint_start=self.n_joints,
     73     q_start=self.n_qs,
     74     dof_start=self.n_dofs,
     75     geom_start=self.n_geoms,
     76     cell_start=self.n_cells,
     77     vert_start=self.n_verts,
     78     face_start=self.n_faces,
     79     edge_start=self.n_edges,
     80     vgeom_start=self.n_vgeoms,
     81     vvert_start=self.n_vverts,
     82     vface_start=self.n_vfaces,
     83     visualize_contact=visualize_contact,
     84 )
     85 self._entities.append(entity)
     87 return entity

File ~/App/anaconda3/envs/robotic/lib/python3.9/site-packages/genesis/engine/entities/rigid_entity/rigid_entity.py:68, in RigidEntity.__init__(self, scene, solver, material, morph, surface, idx, idx_in_solver, link_start, joint_start, q_start, dof_start, geom_start, cell_start, vert_start, face_start, edge_start, vgeom_start, vvert_start, vface_start, visualize_contact)
     64 self._visualize_contact = visualize_contact
     66 self._is_built = False
---> 68 self._load_model()

File ~/App/anaconda3/envs/robotic/lib/python3.9/site-packages/genesis/engine/entities/rigid_entity/rigid_entity.py:78, in RigidEntity._load_model(self)
     75     self._load_mesh(self._morph, self._surface)
     77 elif isinstance(self._morph, gs.morphs.MJCF):
---> 78     self._load_MJCF(self._morph, self._surface)
     80 elif isinstance(self._morph, gs.morphs.URDF):
     81     self._load_URDF(self._morph, self._surface)

File ~/App/anaconda3/envs/robotic/lib/python3.9/site-packages/genesis/engine/entities/rigid_entity/rigid_entity.py:342, in RigidEntity._load_MJCF(self, morph, surface)
    339         world_g_info.append(g_info)
    341 else:
--> 342     g_info = mju.parse_geom(mj, i_g, morph.scale, morph.convexify, surface, morph.file)
    343     if g_info is not None:
    344         link_idx = mj.geom_bodyid[i_g] - 1

File ~/App/anaconda3/envs/robotic/lib/python3.9/site-packages/genesis/utils/mjcf.py:306, in parse_geom(mj, i_g, scale, convexify, surface, xml_path)
    304             tex_repeat = mj.mat_texrepeat[mat_id].astype(int)
    305             image_array = np.tile(image_array, (tex_repeat[0], tex_repeat[1], 1))
--> 306             visual = TextureVisuals(uv=uv_coordinates, image=Image.fromarray(image_array))
    307             tmesh.visual = visual
    309 elif mj_type == mujoco.mjtGeom.mjGEOM_MESH:

File ~/App/anaconda3/envs/robotic/lib/python3.9/site-packages/PIL/Image.py:3102, in fromarray(obj, mode)
   3100         typekey_shape, typestr = typekey
   3101         msg = f\"Cannot handle this data type: {typekey_shape}, {typestr}\"
-> 3102         raise TypeError(msg) from e
   3103 else:
   3104     rawmode = mode

TypeError: Cannot handle this data type: (1, 1, 600), |u1"
}

As for the mjcf files of robocasa kitchen environment, I have written some export code to get them.

robocasa_exporter.py
import os
import robosuite
from robosuite.controllers import load_composite_controller_config
from robocasa.environments.kitchen.kitchen import Kitchen
import xml.etree.ElementTree as ET
from typing import List

import xml.etree.ElementTree as ET
from typing import List, Dict

# Author: Riften https://github.com/Riften
# MJCF reference https://mujoco.readthedocs.io/en/stable/XMLreference.html

def indent(elem: ET.Element, level=0):
    i = "\n" + level*"  "
    j = "\n" + (level-1)*"  "
    if len(elem):
        if not elem.text or not elem.text.strip():
            elem.text = i + "  "
        if not elem.tail or not elem.tail.strip():
            elem.tail = i
        for subelem in elem:
            indent(subelem, level+1)
        if not elem.tail or not elem.tail.strip():
            elem.tail = j
    else:
        if level and (not elem.tail or not elem.tail.strip()):
            elem.tail = j
    return elem   

class MJCFHeader:
    """
    A MJCF header contains the bacis xml elements of a mujoco model.
    """
    def __init__(self, xml_str: str):
        root = ET.fromstring(xml_str)
        if root.tag != 'mujoco':
            raise ValueError('Root tag of MJCFHeader must be <mujoco>')
        
        self.option = root.find('option')
        self.compiler = root.find('compiler')
        self.size = root.find('size')
        self.statistic = root.find('statistic')
        # self.actuator = root.find('actuator')
        
        for child in list(root):
            root.remove(child)
        self.root = root
    
    
    def build(self):
        new_root = ET.Element(self.root.tag)
        new_root.attrib['model'] = self.root.attrib.get('model', '')
        for element in ['option', 'compiler', 'size', 'statistic']:
            elem = getattr(self, element)
            if elem is not None:
                new_root.append(elem)
        return new_root
    
class MJCFActuator:
    def __init__(self, xml_str: str):
        root = ET.fromstring(xml_str)
        if root.tag != 'actuator':
            raise ValueError('Root tag of MJCFActuator must be <actuator>')
        self.root = root
        self.joint_actuators = {}
        for child in list(root):
            joint_name = child.attrib.get('joint')
            self.joint_actuators[joint_name] = child
        

class MJCFAsset:
    def __init__(self, xml_str: str):
        root = ET.fromstring(xml_str)
        if root.tag != 'asset':
            raise ValueError('Root tag of MJCFAsset must be <asset>')
        
        self.root = root
        
        self.textures = {}
        self.materials = {}
        self.meshs = {}
        for child in list(root):
            if child.tag == 'texture':
                if 'name' in child.attrib:
                    #raise ValueError('Texture element must have a name attribute')
                    self.textures[child.attrib['name']] = child
                else:
                    print('Texture element does not have a name attribute')
            elif child.tag == 'material':
                self.materials[child.attrib['name']] = child
            elif child.tag == 'mesh':
                self.meshs[child.attrib['name']] = child
    
    def retrive_body_requirement(self, 
            body: ET.Element,
            required_materials: Dict[str, ET.Element],
            required_textures: Dict[str, ET.Element],
            required_meshs: Dict[str, ET.Element],
            ):
        for child in list(body):
            if child.tag == 'body':
                self.retrive_body_requirement(child, required_materials, required_textures, required_meshs)
            if child.tag == 'geom':
                material = child.attrib.get('material')
                if material is not None:
                    if material not in required_materials:
                        # check if the material is defined in the assets
                        if material not in self.materials:
                            print(f'Material {material} is not defined in assets.')
                        required_materials[material] = self.materials[material]
                        texture = self.materials[material].attrib.get('texture')
                        if texture is not None:
                            if texture not in required_textures:
                                if texture not in self.textures:
                                    print(f'Texture {texture} is not defined in assets.')
                                required_textures[texture] = self.textures[texture]
                    
                
                mesh = child.attrib.get('mesh')
                if mesh is not None:
                    if mesh not in required_meshs:
                        if mesh not in self.meshs:
                            print(f'Mesh {mesh} is not defined in assets.')
                        required_meshs[mesh] = self.meshs[mesh]
        
    
def retrive_body_requirement_recursive(body: ET.Element, 
            required_materials: List[str],
            ):
    """
    Recursively retrive the body requirements from the body element.
    """
    for child in list(body):
        if child.tag == 'body':
            retrive_body_requirement_recursive(child, required_materials)
        if child.tag == 'geom':
            material = child.attrib.get('material')
            if material is not None:
                required_materials.append(material)

def remove_geom_group_0(root: ET.Element):
    for child in list(root):
        if child.tag == 'geom':
            if 'group' in child.attrib and child.attrib['group'] == '0':
                root.remove(child)
        else:
            remove_geom_group_0(child)

def build_mjcf_from_body_elements(body_elements: List[ET.Element],
                                header: MJCFHeader, 
                                assets: MJCFAsset, 
                                actuator: MJCFActuator = None,
                                remove_group_0 = True
                                ):
    root = header.build()
    
    required_materials: Dict[str, ET.Element] = {}
    required_textures: Dict[str, ET.Element] = {}
    required_meshs: Dict[str, ET.Element] = {}
            
    for body in body_elements:
        assets.retrive_body_requirement(body, required_materials, required_textures, required_meshs)
        
        
    asset_element = ET.Element('asset')
    for material in required_materials.values():
        asset_element.append(material)
    for texture in required_textures.values():
        asset_element.append(texture)
    for mesh in required_meshs.values():
        asset_element.append(mesh)
    
    root.append(asset_element)
    if actuator is not None:
        root.append(actuator.root)
        
    worldbody = ET.Element('worldbody')
    for body in body_elements:
        worldbody.append(body)

    
    root.append(worldbody)
    
    if remove_group_0:
        remove_geom_group_0(root)
    
    return root

_UNSUPPORTED_FIXTURES = {
    'paper_towel_main_group': 'ValueError: Incorrect texture coordinate shape',
    'outlet_room': 'ValueError: Cannot reformat 2-channel texture into RGBA',
    'outlet_2_room': 'ValueError: Cannot reformat 2-channel texture into RGBA',
    'coffee_machine_main_group': 'ValueError: Cannot reformat 2-channel texture into RGBA'
}

def load_robocasa_env(layout: int=1, style: int=1) -> Kitchen:
    config = {
        "env_name": "PnPCounterToCab",
        "robots": "PandaOmron",
        "controller_configs": load_composite_controller_config(robot="PandaOmron"),
        "translucent_robot": False,
    }
    # there is default layout and texture
    env: Kitchen = robosuite.make(
        **config,
        has_renderer=True,
        has_offscreen_renderer=False,
        render_camera=None,
        ignore_done=True,
        use_camera_obs=False,
        control_freq=20,
        renderer="mjviewer",
    )
    env.layout_and_style_ids = [[layout, style]]
    _ = env.reset()
    return env

def set_group_transparency(root: ET.Element, transparent_group: List[int] = [0], alpha: float = 0.0):
    for geom in root.findall('.//geom'):
        group = int(geom.get('group', -1))
        if group in transparent_group:
            geom.set('rgba', f'1 1 1 {alpha}')

def export_fixtures_to_dir(env: Kitchen, out_dir: str,
                           transparent_group: List[int] = [0]):
    
    env_xml_str = env.model.get_xml()
    root = ET.fromstring(env_xml_str)
    asset_element = root.find('asset')
    header = MJCFHeader(env_xml_str)
    asset = MJCFAsset(ET.tostring(asset_element))
    
    for fixture_name in env.fixtures:
        if fixture_name in _UNSUPPORTED_FIXTURES:
            print(f"Skipping {fixture_name} due to {_UNSUPPORTED_FIXTURES[fixture_name]}")
            continue
        fixture = env.fixtures[fixture_name]
        fixture_mjcf = build_mjcf_from_body_elements([fixture.get_obj()], header, asset, 
                                                    remove_group_0 = False)
        set_group_transparency(fixture_mjcf, transparent_group, alpha=0.0)
        with open(os.path.join(out_dir, f'{fixture_name}.xml'), 'w') as f:
            f.write(ET.tostring(indent(fixture_mjcf), encoding='unicode'))
            
            
if __name__ == '__main__':
    env = load_robocasa_env(1,1)
    os.makedirs('fixtures', exist_ok=True)
    export_fixtures_to_dir(env, 'fixtures')

Feel free to use it.

Riften avatar Dec 24 '24 11:12 Riften

I fixed PR #288 to support gray images as textures. Please give it a try. Thanks for providing the code. I randomly loaded 5 fixtures, and it looks good to me. Let me know if there are any further issues.

zhenjia-xu avatar Dec 24 '24 22:12 zhenjia-xu

I fixed PR #288 to support gray images as textures. Please give it a try. Thanks for providing the code. I randomly loaded 5 fixtures, and it looks good to me. Let me know if there are any further issues.

Textures are loaded, while the position and scale of geoms seems to be wrong.

before

https://github.com/user-attachments/assets/15752613-e33f-4b1b-90fa-f6eb90cf30b7

now

https://github.com/user-attachments/assets/688786bc-bb7d-44a6-9920-0d0114cc31d9

I just load all the fixtures exported.

def load_mjcf_dir(scene: gs.Scene, load_dir: str):
    for mjcf_filename in os.listdir(load_dir):
        if not mjcf_filename.endswith('.xml'):
            continue
        mjcf_morph = gs.morphs.MJCF(file=os.path.join(load_dir, mjcf_filename))
        scene.add_entity(mjcf_morph)

It seems that the box created has different size than before. Here is the vertices coordinate for mjcf in texture_bug.zip

scene = gs.Scene(show_viewer=True)
mjcf_morph = gs.morphs.MJCF(file=os.path.join('texture_bug', 'floor_room.xml'))
mjcf_entity = scene.add_entity(mjcf_morph)
scene.build()
mjcf_entity.vgeoms[0].get_trimesh().vertices

result before

TrackedArray([[-2.54, -3.19, -0.02],
              [-2.54, -3.19,  0.02],
              [-2.54,  3.19, -0.02],
              [-2.54,  3.19,  0.02],
              [ 2.54, -3.19, -0.02],
              [ 2.54, -3.19,  0.02],
              [ 2.54,  3.19, -0.02],
              [ 2.54,  3.19,  0.02]])

result now

TrackedArray([[ 0.  ,  0.  , -0.02],
              [ 0.  ,  0.  ,  0.02],
              [ 0.  ,  1.  , -0.02],
              [ 0.  ,  1.  ,  0.02],
              [ 1.  ,  0.  , -0.02],
              [ 1.  ,  0.  ,  0.02],
              [ 1.  ,  1.  , -0.02],
              [ 1.  ,  1.  ,  0.02]])

Riften avatar Dec 25 '24 05:12 Riften

Hi Riften, the bug has been fixed #288. Sorry for the inconvenience.

zhenjia-xu avatar Dec 27 '24 03:12 zhenjia-xu

It works! Thank you very much for the timely fix! I will close this issue shortly.

bug_fixed

Riften avatar Dec 27 '24 03:12 Riften

great! I will merge into main.

zhenjia-xu avatar Dec 27 '24 06:12 zhenjia-xu