File size: 10,075 Bytes
24f5025
 
7c39d15
24f5025
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7c39d15
24f5025
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6e37f5c
24f5025
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7c39d15
24f5025
 
6e37f5c
24f5025
 
 
 
 
 
 
 
7c39d15
37aeb5b
267f540
 
24f5025
 
 
37aeb5b
 
 
 
 
 
 
 
5a3e910
 
37aeb5b
 
 
 
04f25a3
7e5091e
 
 
 
 
 
fc219b8
37aeb5b
 
 
 
0c552a7
158c388
37aeb5b
 
 
 
24f5025
37aeb5b
 
69ac8ac
37aeb5b
 
 
 
 
 
 
 
24f5025
37aeb5b
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
import os
import sys
import subprocess
import tempfile
import spaces

# Build pytorch3d and nvdiffrast against the installed torch the first time a GPU is allocated.
# Both packages publish no prebuilt wheels for torch >= 2.8 (Blackwell / CUDA 13 stack).
CUDA_HOME = "/cuda-image/usr/local/cuda-13.0"


def _build_env():
    env = os.environ.copy()
    env["CUDA_HOME"] = CUDA_HOME
    env["CUDA_PATH"] = CUDA_HOME
    env["PATH"] = os.path.join(CUDA_HOME, "bin") + os.pathsep + env.get("PATH", "")
    env["TORCH_CUDA_ARCH_LIST"] = "12.0"
    return env


def _ensure_pkg(name, install_args):
    try:
        __import__(name)
        return True
    except ImportError:
        pass
    subprocess.check_call(
        [sys.executable, "-m", "pip", "install",
         "--no-build-isolation",
         "--no-deps",
         *install_args],
        env=_build_env(),
    )


def _strip_pulsar_from_pytorch3d_src(src):
    """Drop pulsar from a cloned pytorch3d source tree.

    Pulsar fails to link on modern GCC because the explicit template
    instantiations of pulsar::Renderer::fill_bg<true>/forward<true>/...
    end up as hidden symbols and ld errors with:

        hidden symbol `..._fill_bg<true>...' isn't defined
        final link failed: bad value

    Unique3D never imports pulsar (only pytorch3d.renderer.{cameras,mesh,...}
    and pytorch3d.structures). Strip pulsar entirely before building.
    """
    import shutil

    # 1. Remove the C++/CUDA sources so setup.py's glob skips them.
    pulsar_csrc = os.path.join(src, "pytorch3d", "csrc", "pulsar")
    if os.path.isdir(pulsar_csrc):
        shutil.rmtree(pulsar_csrc)

    # 2. Remove the python-side pulsar package.
    pulsar_py = os.path.join(src, "pytorch3d", "renderer", "points", "pulsar")
    if os.path.isdir(pulsar_py):
        shutil.rmtree(pulsar_py)

    # 3. Rewrite ext.cpp to drop every pulsar-related include and the entire
    # pulsar pybind11 registration block. Use the well-defined "Pulsar." comment
    # marker that starts the registration block and run to the matching #endif.
    ext_cpp = os.path.join(src, "pytorch3d", "csrc", "ext.cpp")
    if os.path.isfile(ext_cpp):
        with open(ext_cpp, "r") as f:
            lines = f.readlines()

        # Phase A: drop the three pulsar include lines (they sit inside
        # `#if !defined(USE_ROCM)` blocks together with non-pulsar includes? No —
        # check the file: the top of ext.cpp has two #if !defined(USE_ROCM)
        # blocks that only contain pulsar includes. Drop the whole #if/#endif
        # pairs that contain only pulsar includes.
        out = []
        i = 0
        while i < len(lines):
            ln = lines[i]
            if ln.strip().startswith("#if !defined(USE_ROCM)"):
                # Look ahead for the matching #endif. Collect block body.
                j = i + 1
                block = []
                while j < len(lines) and not lines[j].strip().startswith("#endif"):
                    block.append(lines[j])
                    j += 1
                # Decide: if every non-blank/non-comment line in block mentions
                # "pulsar", drop the whole #if..#endif. Otherwise keep as-is.
                meaningful = [
                    b for b in block
                    if b.strip() and not b.strip().startswith("//")
                ]
                if meaningful and all("pulsar" in b.lower() for b in meaningful):
                    # Drop block (and matching #endif).
                    i = j + 1
                    continue
            out.append(ln)
            i += 1

        # Phase B: drop the pulsar pybind registration block.
        # Find the "// Pulsar." comment, walk back to its preceding blank line,
        # then forward to the matching #endif (the last #endif before `}`).
        text = "".join(out)
        marker = "  // Pulsar."
        idx = text.find(marker)
        if idx >= 0:
            # Find the start of the line containing the marker.
            line_start = text.rfind("\n", 0, idx) + 1
            # Find the trailing `#endif` of the pulsar block. Walk forward; the
            # block ends with a `#endif` on a line by itself, right before `}`.
            end_marker = "\n#endif\n}"
            end_idx = text.find(end_marker, line_start)
            if end_idx < 0:
                # Fall back: just drop to the next `}` on its own line.
                end_idx = text.find("\n}\n", line_start)
                if end_idx < 0:
                    raise RuntimeError("could not locate pulsar block end in ext.cpp")
                # Keep the `}`.
                text = text[:line_start] + text[end_idx + 1:]
            else:
                # Drop the `#endif` line but keep the `}`.
                text = text[:line_start] + text[end_idx + len("\n#endif"):]

        with open(ext_cpp, "w") as f:
            f.write(text)

    # 4. Patch renderer/points/__init__.py AND renderer/__init__.py: both have a
    # `if not torch.version.hip:` block that imports PulsarPointsRenderer; replace
    # the import with `pass` so the symbol is just absent.
    for init_path in [
        os.path.join(src, "pytorch3d", "renderer", "points", "__init__.py"),
        os.path.join(src, "pytorch3d", "renderer", "__init__.py"),
    ]:
        if not os.path.isfile(init_path):
            continue
        with open(init_path, "r") as f:
            init_lines = f.readlines()
        new_lines = []
        skip_block = False
        for ln in init_lines:
            if "torch.version.hip" in ln and ln.lstrip().startswith("if "):
                indent = " " * (len(ln) - len(ln.lstrip()))
                new_lines.append(ln)
                new_lines.append(indent + "    pass  # pulsar disabled (Blackwell linker)\n")
                skip_block = True
                continue
            if skip_block:
                # Drop any indented continuation lines of the if-block, including
                # multi-line `from .points import (` parenthesized imports.
                stripped = ln.strip()
                if stripped == "" or ln.startswith(" ") or ln.startswith("\t"):
                    if "pulsar" in ln.lower() or stripped == "" or stripped.startswith(")") or stripped.startswith("from ") or stripped.startswith("import "):
                        # part of the pulsar import block — drop
                        continue
                    skip_block = False
                else:
                    skip_block = False
            new_lines.append(ln)
        with open(init_path, "w") as f:
            f.writelines(new_lines)


@spaces.GPU(duration=300)
def _build_pytorch3d():
    # Probe the actual import path Unique3D uses; if the previously installed
    # pytorch3d still has a broken `renderer/__init__.py` (e.g. left a dangling
    # `from .points import PulsarPointsRenderer`), uninstall and rebuild.
    try:
        from pytorch3d.renderer import TexturesVertex  # noqa: F401
        return
    except Exception:
        pass
    subprocess.call(
        [sys.executable, "-m", "pip", "uninstall", "-y", "pytorch3d"],
    )

    src = tempfile.mkdtemp(prefix="pytorch3d_src_")
    subprocess.check_call(
        ["git", "clone", "--depth=1", "--branch=stable",
         "https://github.com/facebookresearch/pytorch3d.git", src],
    )
    _strip_pulsar_from_pytorch3d_src(src)

    # Force a fresh wheel build (don't reuse the cached broken wheel from a
    # previous run, which still has the pulsar import in renderer/__init__.py).
    subprocess.check_call(
        [sys.executable, "-m", "pip", "install",
         "--no-build-isolation", "--no-deps",
         "--force-reinstall", "--no-cache-dir", src],
        env=_build_env(),
    )


@spaces.GPU(duration=300)
def _build_nvdiffrast():
    _ensure_pkg("nvdiffrast", ["git+https://github.com/NVlabs/nvdiffrast.git"])


# Run setup before importing anything that needs these packages.
_build_pytorch3d()
_build_nvdiffrast()


if __name__ == "__main__":
    from huggingface_hub import login
    hf_token = os.environ.get("HF_TOKEN")
    if hf_token:
        login(token=hf_token)

    sys.path.append(os.curdir)
    import torch
    torch.set_float32_matmul_precision('medium')
    torch.backends.cuda.matmul.allow_tf32 = True
    torch.set_grad_enabled(False)

import fire
import gradio as gr
from gradio_app.gradio_3dgen import create_ui as create_3d_ui
from gradio_app.all_models import model_zoo


_TITLE = '''Unique3D: High-Quality and Efficient 3D Mesh Generation from a Single Image'''
_DESCRIPTION = '''

<div>
    <a style="display:inline-block" href='https://github.com/AiuniAI/Unique3D'><img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/AiuniAI/Unique3D?style=social">
</a>
    <img alt="GitHub License" src="https://img.shields.io/github/license/AiuniAI/Unique3D">
</div>

# [Paper](https://arxiv.org/abs/2405.20343) | [Project page](https://wukailu.github.io/Unique3D/) | [Huggingface Demo](https://huggingface.co/spaces/Wuvin/Unique3D) | [Gradio Demo](http://unique3d.demo.avar.cn/) | [Online Demo](https://www.aiuni.ai/)

* High-fidelity and diverse textured meshes generated by Unique3D from single-view images.

* The demo is still under construction, and more features are expected to be implemented soon.

* If the Huggingface Demo is overcrowded or fails to produce stable results, you can use the Online Demo [aiuni.ai](https://www.aiuni.ai/), which is free to try (get the registration invitation code Join Discord: https://discord.gg/aiuni). However, the Online Demo is slightly different from the Gradio Demo, in that the inference speed is slower, but the generation is much more stable.
'''

def launch():
    model_zoo.init_models()

    with gr.Blocks(
        title=_TITLE,
        # theme=gr.themes.Monochrome(),
    ) as demo:
        with gr.Row():
            with gr.Column(scale=1):
                gr.Markdown('# ' + _TITLE)
        gr.Markdown(_DESCRIPTION)
        create_3d_ui("wkl")

    demo.queue().launch(share=True)

if __name__ == '__main__':
    fire.Fire(launch)