PanoCraft / app.py
szili2011's picture
Update app.py
6d09191 verified
import gradio as gr
import torch
from transformers import DPTForDepthEstimation, DPTImageProcessor
from PIL import Image
import numpy as np
import trimesh
from nbtschematic import SchematicFile
from scipy.spatial import cKDTree
import os
import shutil
import zipfile
# --- NEW IMPORTS for .mcworld export ---
# Initialize to a safe default state
AMULET_AVAILABLE = False
PALETTE_AMULET_BLOCKS = []
try:
import amulet
import amulet.api.world
from amulet.api.selection import SelectionGroup, SelectionBox
from amulet.api.structure import Structure
from amulet.api.block import Block
# If imports succeed, set the flag to True
AMULET_AVAILABLE = True
print("Amulet-core loaded successfully.")
except ImportError:
# If imports fail, just print a warning. The flag remains False.
print("Amulet-core not found. .mcworld export will be disabled.")
# --- 1. SETUP ---
print("Initializing... Loading AI model.")
processor = DPTImageProcessor.from_pretrained("Intel/dpt-hybrid-midas")
model = DPTForDepthEstimation.from_pretrained("Intel/dpt-hybrid-midas")
device = "cuda" if torch.cuda.is_available() else "cpu"
model.to(device)
print(f"Model loaded to device: {device}")
MINECRAFT_PALETTE_DATA = {
"Stone": (("minecraft", "stone"), (1, 0), (128, 128, 128)),
"Cobblestone": (("minecraft", "cobblestone"), (4, 0), (125, 125, 125)),
"Gravel": (("minecraft", "gravel"), (13, 0), (136, 132, 131)),
"Andesite": (("minecraft", "stone", {"stone_type": "andesite"}), (1, 5), (136, 136, 136)),
"Iron Block": (("minecraft", "iron_block"), (42, 0), (218, 218, 218)),
"Clay Block": (("minecraft", "clay"), (82, 0), (164, 172, 183)),
"Light Gray Wool": (("minecraft", "wool", {"color": "light_gray"}), (35, 8), (142, 142, 134)),
"Gray Wool": (("minecraft", "wool", {"color": "gray"}), (35, 7), (65, 68, 72)),
"Black Wool": (("minecraft", "wool", {"color": "black"}), (35, 15), (21, 21, 26)),
"White Wool": (("minecraft", "wool", {"color": "white"}), (35, 0), (234, 236, 237)),
"Oak Planks": (("minecraft", "planks", {"wood_type": "oak"}), (5, 0), (157, 128, 79)),
"Spruce Planks": (("minecraft", "planks", {"wood_type": "spruce"}), (5, 1), (101, 75, 43)),
"Birch Planks": (("minecraft", "planks", {"wood_type": "birch"}), (5, 2), (193, 174, 114)),
"Dark Oak Planks": (("minecraft", "planks", {"wood_type": "dark_oak"}), (5, 5), (61, 42, 21)),
"Dirt": (("minecraft", "dirt"), (3, 0), (134, 96, 67)),
"Red Wool": (("minecraft", "wool", {"color": "red"}), (35, 14), (168, 50, 51)),
"Sand": (("minecraft", "sand"), (12, 0), (218, 211, 160)),
"Grass Block": (("minecraft", "grass_block"), (2, 0), (123, 182, 74)),
"Blue Wool": (("minecraft", "wool", {"color": "blue"}), (35, 11), (53, 57, 157)),
"Light Blue Wool": (("minecraft", "wool", {"color": "light_blue"}), (35, 3), (105, 158, 210)),
"Obsidian": (("minecraft", "obsidian"), (49, 0), (22, 18, 29)),
}
# --- This logic is now conditional ---
PALETTE_VALUES = list(MINECRAFT_PALETTE_DATA.values())
if AMULET_AVAILABLE:
# Only create the Amulet Block list if the library was imported successfully
PALETTE_AMULET_BLOCKS = [Block(v[0][0], v[0][1], v[0][2] if len(v[0]) > 2 else {}) for v in PALETTE_VALUES]
PALETTE_SCHEMATIC_BLOCKS = np.array([v[1] for v in PALETTE_VALUES])
PALETTE_COLORS = np.array([v[2] for v in PALETTE_VALUES])
COLOR_TREE = cKDTree(PALETTE_COLORS)
print("Palette initialized.")
# --- CORE & EXPORT FUNCTIONS (Unchanged from previous attempt) ---
def predict_depth(image):
inputs = processor(images=image, return_tensors="pt").to(device)
with torch.no_grad():
outputs = model(**inputs)
predicted_depth = outputs.predicted_depth
prediction = torch.nn.functional.interpolate(
predicted_depth.unsqueeze(1), size=image.size[::-1], mode="bicubic", align_corners=False
)
output = prediction.squeeze().cpu().numpy()
return (output - output.min()) / (output.max() - output.min())
def create_point_cloud_from_depth(depth_map, image, max_voxels=128):
h, w = depth_map.shape
scale_factor = np.sqrt((max_voxels**2) / (w * h)) if w*h > 0 else 0
new_w, new_h = int(w * scale_factor), int(h * scale_factor)
if new_w == 0 or new_h == 0: return np.array([]), np.array([])
small_depth = np.array(Image.fromarray(depth_map).resize((new_w, new_h)))
small_image = image.resize((new_w, new_h))
colors = np.array(small_image).reshape(-1, 3)
y, x = np.mgrid[:new_h, :new_w]
lon = (x / new_w) * 2 * np.pi - np.pi
lat = -(y / new_h) * np.pi + np.pi / 2
r = small_depth * (max_voxels / 2)
px = r * np.cos(lat) * np.cos(lon)
py = r * np.cos(lat) * np.sin(lon)
pz = r * np.sin(lat)
points = np.stack([px, py, pz], axis=-1).reshape(-1, 3)
points -= points.mean(axis=0)
return points, colors
def voxelize(points, colors, pitch=1.0):
if len(points) == 0: return np.array([]), np.array([])
scaled_points = points / pitch
cloud = trimesh.points.PointCloud(scaled_points, colors=colors)
voxel_grid = trimesh.voxel.VoxelGrid(cloud)
if not isinstance(voxel_grid, trimesh.voxel.VoxelGrid): return np.array([]), np.array([])
voxel_coords, voxel_colors = voxel_grid.matrix_to_frame()
if len(voxel_colors) == 0: return np.array([]), np.array([])
_, indices = COLOR_TREE.query(voxel_colors[:, :3])
return voxel_coords, indices
def create_schematic(voxel_coords, palette_indices, filename):
if len(voxel_coords) == 0:
sf = SchematicFile(shape=(1, 1, 1)); sf.save(filename)
return filename
voxel_block_data = PALETTE_SCHEMATIC_BLOCKS[palette_indices]
min_c = voxel_coords.min(axis=0); max_c = voxel_coords.max(axis=0)
dims = (max_c - min_c + 1).astype(int)
sf = SchematicFile(shape=(dims[2], dims[1], dims[0]))
for i, coord in enumerate(voxel_coords):
x, y, z = (coord - min_c).astype(int)
block_id, block_data = voxel_block_data[i]
if 0 <= z < sf.shape[0] and 0 <= y < sf.shape[1] and 0 <= x < sf.shape[2]:
sf.Blocks[z, y, x] = block_id
sf.Data[z, y, x] = block_data
sf.save(filename)
return filename
def create_mcworld(voxel_coords, palette_indices, filename):
if not AMULET_AVAILABLE:
raise gr.Error("Amulet library failed to load. Cannot create .mcworld files.")
TEMP_WORLD_DIR = "./temp_mc_world"
if os.path.exists(TEMP_WORLD_DIR): shutil.rmtree(TEMP_WORLD_DIR)
world = amulet.load_format(TEMP_WORLD_DIR)
world.create_and_open("bedrock", (0, 255))
if len(voxel_coords) > 0:
min_c = voxel_coords.min(axis=0)
selection_box = SelectionBox(min_c, (voxel_coords.max(axis=0) - min_c + 1))
structure_array = np.zeros(selection_box.shape, dtype=object)
for i, coord in enumerate(voxel_coords):
x, y, z = (coord - min_c).astype(int)
block = PALETTE_AMULET_BLOCKS[palette_indices[i]]
structure_array[x, y, z] = (block, None)
structure = Structure(SelectionGroup(selection_box), structure_array, {})
world.put_structure(structure, world.dimensions[0])
world.save(); world.close()
with zipfile.ZipFile(filename, 'w', zipfile.ZIP_DEFLATED) as zipf:
for root, dirs, files in os.walk(TEMP_WORLD_DIR):
for file in files:
filepath = os.path.join(root, file)
zipf.write(filepath, filepath.replace(TEMP_WORLD_DIR, ''))
shutil.rmtree(TEMP_WORLD_DIR)
return filename
# --- MAIN LOGIC & UI ---
def panorama_to_minecraft(pano_image, max_dim, output_format):
if pano_image is None: raise gr.Error("Please upload a 360° panorama image.")
if output_format == ".mcworld" and not AMULET_AVAILABLE:
raise gr.Error("Amulet library is not available in this environment. Please choose .schematic instead.")
print("Step 1/4: Predicting Depth...")
depth_map = predict_depth(pano_image)
print("Step 2/4: Creating 3D Point Cloud...")
points, colors = create_point_cloud_from_depth(depth_map, pano_image, max_voxels=max_dim)
print("Step 3/4: Voxelizing Scene...")
voxel_coords, palette_indices = voxelize(points, colors, pitch=1.0)
print(f"Step 4/4: Creating {output_format} file...")
if not os.path.exists("outputs"): os.makedirs("outputs")
if output_format == ".mcworld":
output_filename = os.path.join("outputs", "PanoCraft.mcworld")
schematic_path = create_mcworld(voxel_coords, palette_indices, output_filename)
else:
output_filename = os.path.join("outputs", "PanoCraft.schematic")
schematic_path = create_schematic(voxel_coords, palette_indices, output_filename)
print("Processing complete.")
return schematic_path
with gr.Blocks(title="PanoCraft") as demo:
gr.Markdown("# 360° Panorama to Minecraft Converter")
gr.Markdown("Upload a 360° photo to create a Minecraft model. Export as a `.schematic` for editors like Amulet, or as a `.mcworld` to import directly into Bedrock Edition (PE, Windows, etc).")
with gr.Row():
with gr.Column(scale=1):
image_input = gr.Image(type="pil", label="Upload 360° Panorama Image")
max_dim_slider = gr.Slider(minimum=32, maximum=256, value=128, step=16, label="Detail Level", info="Higher values create more detailed models.")
output_format_radio = gr.Radio(
[".schematic", ".mcworld"], value=".schematic", label="Output Format", info=".mcworld is for direct Bedrock import."
)
submit_btn = gr.Button("Generate Minecraft File", variant="primary")
with gr.Column(scale=1):
file_output = gr.File(label="Download File")
submit_btn.click(
fn=panorama_to_minecraft,
inputs=[image_input, max_dim_slider, output_format_radio],
outputs=file_output
)
gr.Markdown("### Example")
gr.Examples(
examples=[[os.path.join(os.path.dirname(__file__), "example_pano.jpg")]],
inputs=[image_input],
label="Click to try"
)
demo.launch()