from __future__ import annotations from pathlib import Path from PIL import Image import streamlit as st from mine2real.inference import ( CHECKPOINT_PATH, MINECRAFT_DIR, REAL_DIR, TranslationResult, get_device, list_example_images, load_model, translate_image, ) st.set_page_config( page_title="mine2real", page_icon="⛏️", layout="wide", ) CUSTOM_CSS = """ """ @st.cache_resource(show_spinner="Loading CycleGAN checkpoint...") def get_model(): device = get_device() model = load_model(CHECKPOINT_PATH, device=device) return model, device @st.cache_data(show_spinner=False) def load_example_gallery() -> dict[str, list[Path]]: return { "minecraft": list_example_images(MINECRAFT_DIR, limit=5, seed=7), "real": list_example_images(REAL_DIR, limit=5, seed=11), } def render_result(result: TranslationResult, input_label: str, output_label: str) -> None: col1, col2, col3 = st.columns(3) col1.image(result.source, caption=input_label, width="stretch") col2.image(result.translated, caption=output_label, width="stretch") col3.image( result.reconstructed, caption="Cycle reconstruction", width="stretch", ) def app() -> None: st.markdown(CUSTOM_CSS, unsafe_allow_html=True) st.markdown( """

mine2real

CycleGAN-приложение для перевода пейзажей между Minecraft и реальными природными сценами.

Domain A: Minecraft forestlike
Domain B: Real nature landscape
Inference: bidirectional
Deployment: Hugging Face Space via Docker
""", unsafe_allow_html=True, ) model, device = get_model() meta_col1, meta_col2, meta_col3, meta_col4 = st.columns(4) meta_col1.markdown( """
Checkpoint
cycle_gan_color_fix#0_epoch_10.pt
""", unsafe_allow_html=True, ) meta_col2.markdown( f"""
Runtime
{device}
""", unsafe_allow_html=True, ) meta_col3.markdown( """
Input Format
Any image will be center-cropped and resized to 256x256 before translation.
""", unsafe_allow_html=True, ) meta_col4.markdown( """
Dataset
Yandex Disk
""", unsafe_allow_html=True, ) st.write("") st.subheader("Translate Image") direction_labels = { "minecraft_to_real": "Minecraft -> Real -> Minecraft", "real_to_minecraft": "Real -> Minecraft -> Real", } if hasattr(st, "segmented_control"): direction = st.segmented_control( "Direction", options=list(direction_labels.keys()), default="minecraft_to_real", selection_mode="single", format_func=lambda value: direction_labels[value], width="stretch", ) else: direction = st.radio( "Direction", options=list(direction_labels.keys()), format_func=lambda value: direction_labels[value], horizontal=True, ) uploaded = st.file_uploader( "Upload one image from one of the domains", type=["png", "jpg", "jpeg", "webp"], accept_multiple_files=False, help="The app accepts exactly one image and runs a full CycleGAN pass with reconstruction.", ) run_button = st.button("Translate image", type="primary", width="stretch") if uploaded is not None: image = Image.open(uploaded).convert("RGB") if run_button: with st.spinner("Generating translation..."): result = translate_image(model, image, direction=direction, device=device) if direction == "minecraft_to_real": input_label = "A: Uploaded Minecraft scene" output_label = "B: Translated real landscape" else: input_label = "B: Uploaded real landscape" output_label = "A: Translated Minecraft-like scene" render_result(result, input_label, output_label) st.warning( """ Качество результата всё ещё оставляет желать лучшего. Это ожидаемо для CycleGAN: модель хорошо переносит общую структуру сцены, но заметно слабее справляется с мелкими деталями, локальными текстурами и стабильной цветопередачей. Часто видны такие проблемы: - смазывание или потеря мелких объектов и фактуры; - упрощение геометрии и превращение деталей в более грубые пятна; - цветовые сдвиги, неестественные оттенки и местами "грязная" палитра. Что удалось частично улучшить в обучении: - `ReplayBuffer` помог сделать обучение дискриминатора стабильнее и немного снизить резкие артефакты и хаотичные скачки стиля; - `VggPerceptualLoss` / perceptual fine-tuning помог лучше сохранять общую визуальную структуру и частично уменьшил самые грубые проблемы с цветом: менее случайные цветовые сдвиги, меньше выцветания и чуть более связная палитра. Но полностью убрать эти ограничения не удалось: для тонкой прорисовки деталей и аккуратной работы с цветом базовый CycleGAN всё равно довольно ограничен. """ ) else: st.image(image, caption="Uploaded image", width="stretch") else: st.info("Upload an image and click `Translate image` to run the model.") st.write("") st.subheader("Submission Artifacts") st.markdown( """ - Notebook: `notebooks/lab5.ipynb` - Checkpoint: `checkpoints/cycle_gan_color_fix#0_epoch_10.pt` - Example gallery in repo: `demo_gallery/` (5 + 5 random images); full training data stays under `data/datasets/` when unpacked locally """ ) st.write("") st.markdown("---") st.subheader("Dataset Examples") st.caption("Reference samples from both domains. This section is intentionally separated from the working inference interface.") galleries = load_example_gallery() if not galleries["minecraft"] and not galleries["real"]: st.info( "Example gallery is empty: ensure `demo_gallery/` is present in the app checkout, or restore `data/datasets/` from the Yandex Disk link above." ) block_a, block_b = st.columns(2) with block_a: st.markdown( """
Domain A
Minecraft forestlike
""", unsafe_allow_html=True, ) st.write("") cols = st.columns(2) for idx, image_path in enumerate(galleries["minecraft"]): cols[idx % 2].image(str(image_path), caption=image_path.name, width="stretch") with block_b: st.markdown( """
Domain B
Real nature landscape
""", unsafe_allow_html=True, ) st.write("") cols = st.columns(2) for idx, image_path in enumerate(galleries["real"]): cols[idx % 2].image(str(image_path), caption=image_path.name, width="stretch") if __name__ == "__main__": app()