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"""
""",
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(
"""
""",
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()