| | 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 |
| |
|
| | |
| |
|
| | |
| | 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 |
| | |
| | |
| | AMULET_AVAILABLE = True |
| | print("Amulet-core loaded successfully.") |
| |
|
| | except ImportError: |
| | |
| | print("Amulet-core not found. .mcworld export will be disabled.") |
| |
|
| |
|
| | |
| |
|
| | 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)), |
| | } |
| |
|
| | |
| | PALETTE_VALUES = list(MINECRAFT_PALETTE_DATA.values()) |
| | if AMULET_AVAILABLE: |
| | |
| | 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.") |
| |
|
| |
|
| | |
| |
|
| | 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 |
| |
|
| |
|
| | |
| |
|
| | 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() |