mine2real / app.py
letitbE's picture
feat: add dataset Yandex link card and tidy submission notes
9a24b4b
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 = """
<style>
:root {
--bg: #f5f1e8;
--panel: rgba(255, 252, 245, 0.86);
--ink: #17211c;
--accent: #3f7d4c;
--accent-2: #cc7a00;
--line: rgba(23, 33, 28, 0.12);
}
.stApp {
background:
radial-gradient(circle at top left, rgba(204, 122, 0, 0.18), transparent 28%),
radial-gradient(circle at top right, rgba(63, 125, 76, 0.18), transparent 26%),
linear-gradient(180deg, #f3efe4 0%, #ece7da 100%);
color: var(--ink);
}
.block-container {
padding-top: 2rem;
padding-bottom: 3rem;
max-width: 1200px;
}
.hero {
background: linear-gradient(135deg, rgba(255,255,255,0.76), rgba(245, 234, 212, 0.84));
border: 1px solid var(--line);
border-radius: 24px;
padding: 1.6rem 1.8rem;
box-shadow: 0 20px 70px rgba(44, 52, 46, 0.08);
margin-bottom: 1rem;
backdrop-filter: blur(10px);
}
.hero h1 {
margin: 0 0 0.3rem 0;
font-size: 3rem;
line-height: 1;
}
.hero p {
margin: 0;
font-size: 1.03rem;
}
.pill-row {
display: flex;
gap: 0.7rem;
flex-wrap: wrap;
margin-top: 1rem;
}
.pill {
padding: 0.5rem 0.8rem;
border-radius: 999px;
border: 1px solid var(--line);
background: rgba(255,255,255,0.65);
font-size: 0.9rem;
}
.info-card {
background: var(--panel);
border: 1px solid var(--line);
border-radius: 20px;
padding: 1rem 1.1rem;
min-height: 140px;
}
.section-title {
padding-top: 0.5rem;
font-weight: 700;
letter-spacing: 0.02em;
text-transform: uppercase;
color: #4a564e;
font-size: 0.83rem;
}
</style>
"""
@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(
"""
<div class="hero">
<h1>mine2real</h1>
<p>CycleGAN-приложение для перевода пейзажей между Minecraft и реальными природными сценами.</p>
<div class="pill-row">
<div class="pill">Domain A: Minecraft forestlike</div>
<div class="pill">Domain B: Real nature landscape</div>
<div class="pill">Inference: bidirectional</div>
<div class="pill">Deployment: Hugging Face Space via Docker</div>
</div>
</div>
""",
unsafe_allow_html=True,
)
model, device = get_model()
meta_col1, meta_col2, meta_col3, meta_col4 = st.columns(4)
meta_col1.markdown(
"""
<div class="info-card">
<div class="section-title">Checkpoint</div>
<div><code>cycle_gan_color_fix#0_epoch_10.pt</code></div>
</div>
""",
unsafe_allow_html=True,
)
meta_col2.markdown(
f"""
<div class="info-card">
<div class="section-title">Runtime</div>
<div><code>{device}</code></div>
</div>
""",
unsafe_allow_html=True,
)
meta_col3.markdown(
"""
<div class="info-card">
<div class="section-title">Input Format</div>
<div>Any image will be center-cropped and resized to <code>256x256</code> before translation.</div>
</div>
""",
unsafe_allow_html=True,
)
meta_col4.markdown(
"""
<div class="info-card">
<div class="section-title">Dataset</div>
<div><a href="https://disk.yandex.ru/d/N_G-t-oirnLynw" target="_blank">Yandex Disk</a></div>
</div>
""",
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(
"""
<div class="info-card">
<div class="section-title">Domain A</div>
<div>Minecraft forestlike</div>
</div>
""",
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(
"""
<div class="info-card">
<div class="section-title">Domain B</div>
<div>Real nature landscape</div>
</div>
""",
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()