HANSOL commited on
Commit Β·
d7747e3
1
Parent(s): b2b4324
Server-side 3D rendering with kaleido - no WebGL needed
Browse files- app/main.py +78 -41
- requirements.txt +1 -0
app/main.py
CHANGED
|
@@ -75,6 +75,18 @@ def safe_image(path, caption="", use_column_width=True):
|
|
| 75 |
st.info(f"π· {caption} (μ΄λ―Έμ§ λ―Έν¬ν¨)")
|
| 76 |
|
| 77 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
# ============ μ΄λ‘ μ μ ============
|
| 79 |
|
| 80 |
V_VALLEY_THEORIES = {
|
|
@@ -3119,35 +3131,41 @@ def main():
|
|
| 3119 |
elevation = np.zeros((gallery_grid_size, gallery_grid_size))
|
| 3120 |
|
| 3121 |
with col_view:
|
| 3122 |
-
#
|
| 3123 |
-
|
| 3124 |
-
import matplotlib.colors as mcolors
|
| 3125 |
-
|
| 3126 |
-
fig_2d, ax = plt.subplots(figsize=(8, 8))
|
| 3127 |
-
|
| 3128 |
-
# μ§ν μμ λ§΅
|
| 3129 |
-
cmap = plt.cm.terrain
|
| 3130 |
-
|
| 3131 |
-
# λ¬Όμ΄ μλ μ§νμ νλμ μ€λ²λ μ΄
|
| 3132 |
-
water_mask = elevation < 0
|
| 3133 |
-
|
| 3134 |
-
im = ax.imshow(elevation, cmap=cmap, origin='upper')
|
| 3135 |
-
|
| 3136 |
-
# λ¬Ό μμ νμ
|
| 3137 |
-
if water_mask.any():
|
| 3138 |
-
water_overlay = np.ma.masked_where(~water_mask, np.ones_like(elevation))
|
| 3139 |
-
ax.imshow(water_overlay, cmap='Blues', alpha=0.6, origin='upper')
|
| 3140 |
-
|
| 3141 |
-
ax.set_title(f"{selected_landform}", fontsize=14)
|
| 3142 |
-
ax.axis('off')
|
| 3143 |
-
|
| 3144 |
-
# 컬λ¬λ°
|
| 3145 |
-
cbar = plt.colorbar(im, ax=ax, shrink=0.6, label='κ³ λ (m)')
|
| 3146 |
|
| 3147 |
-
|
| 3148 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3149 |
|
| 3150 |
-
st.caption("π‘
|
| 3151 |
|
| 3152 |
# Educational Description
|
| 3153 |
descriptions = {
|
|
@@ -3208,21 +3226,40 @@ def main():
|
|
| 3208 |
anim_func = ANIMATED_LANDFORM_GENERATORS[landform_key]
|
| 3209 |
stage_elev = anim_func(gallery_grid_size, stage_value)
|
| 3210 |
|
| 3211 |
-
# 2D
|
| 3212 |
-
|
| 3213 |
-
im = ax_2d.imshow(stage_elev, cmap='terrain', origin='upper')
|
| 3214 |
|
| 3215 |
-
|
| 3216 |
-
|
| 3217 |
-
|
| 3218 |
-
|
| 3219 |
-
|
| 3220 |
-
|
| 3221 |
-
|
| 3222 |
-
|
| 3223 |
-
|
| 3224 |
-
|
| 3225 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3226 |
|
| 3227 |
st.caption("π‘ μ¬λΌμ΄λλ₯Ό μ‘°μ νμ¬ νμ± λ¨κ³λ₯Ό νμΈνμΈμ. (0% = μμ, 100% = μμ±)")
|
| 3228 |
|
|
|
|
| 75 |
st.info(f"π· {caption} (μ΄λ―Έμ§ λ―Έν¬ν¨)")
|
| 76 |
|
| 77 |
|
| 78 |
+
# ============ μλ²μ¬μ΄λ 3D λ λλ§ (WebGL μμ΄) ============
|
| 79 |
+
def render_3d_as_image(fig_plotly, width=800, height=600):
|
| 80 |
+
"""Plotly 3D figureλ₯Ό μλ²μ¬μ΄λμμ PNG μ΄λ―Έμ§λ‘ λ λλ§ (WebGL λΆνμ)"""
|
| 81 |
+
import io
|
| 82 |
+
try:
|
| 83 |
+
img_bytes = fig_plotly.to_image(format="png", width=width, height=height, engine="kaleido")
|
| 84 |
+
return img_bytes
|
| 85 |
+
except Exception as e:
|
| 86 |
+
st.error(f"3D λ λλ§ μ€λ₯: {e}")
|
| 87 |
+
return None
|
| 88 |
+
|
| 89 |
+
|
| 90 |
# ============ μ΄λ‘ μ μ ============
|
| 91 |
|
| 92 |
V_VALLEY_THEORIES = {
|
|
|
|
| 3131 |
elevation = np.zeros((gallery_grid_size, gallery_grid_size))
|
| 3132 |
|
| 3133 |
with col_view:
|
| 3134 |
+
# 2D/3D μ ν (μλ²μ¬μ΄λ λ λλ§μΌλ‘ WebGL λ¬Έμ ν΄κ²°)
|
| 3135 |
+
view_mode = st.radio("보기 λͺ¨λ", ["2D νλ©΄λ", "3D μ
체λ (μλ² λ λλ§)"], horizontal=True, key="gallery_view_mode")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3136 |
|
| 3137 |
+
if view_mode == "2D νλ©΄λ":
|
| 3138 |
+
# 2D matplotlib
|
| 3139 |
+
import matplotlib.pyplot as plt
|
| 3140 |
+
fig_2d, ax = plt.subplots(figsize=(8, 8))
|
| 3141 |
+
cmap = plt.cm.terrain
|
| 3142 |
+
water_mask = elevation < 0
|
| 3143 |
+
im = ax.imshow(elevation, cmap=cmap, origin='upper')
|
| 3144 |
+
if water_mask.any():
|
| 3145 |
+
water_overlay = np.ma.masked_where(~water_mask, np.ones_like(elevation))
|
| 3146 |
+
ax.imshow(water_overlay, cmap='Blues', alpha=0.6, origin='upper')
|
| 3147 |
+
ax.set_title(f"{selected_landform}", fontsize=14)
|
| 3148 |
+
ax.axis('off')
|
| 3149 |
+
plt.colorbar(im, ax=ax, shrink=0.6, label='κ³ λ (m)')
|
| 3150 |
+
st.pyplot(fig_2d)
|
| 3151 |
+
plt.close(fig_2d)
|
| 3152 |
+
else:
|
| 3153 |
+
# 3D μλ²μ¬μ΄λ λ λλ§ (WebGL μ¬μ© μ ν¨!)
|
| 3154 |
+
with st.spinner("3D λ λλ§ μ€..."):
|
| 3155 |
+
fig_3d = render_terrain_plotly(
|
| 3156 |
+
elevation,
|
| 3157 |
+
f"{selected_landform} - 3D",
|
| 3158 |
+
add_water=(landform_key in ["delta", "meander", "coastal_cliff", "fjord", "ria_coast", "spit_lagoon"]),
|
| 3159 |
+
water_level=0 if landform_key in ["delta", "coastal_cliff"] else -999,
|
| 3160 |
+
force_camera=True
|
| 3161 |
+
)
|
| 3162 |
+
img_bytes = render_3d_as_image(fig_3d, width=800, height=600)
|
| 3163 |
+
if img_bytes:
|
| 3164 |
+
st.image(img_bytes, caption=f"{selected_landform} - 3D (μλ² λ λλ§)", use_column_width=True)
|
| 3165 |
+
else:
|
| 3166 |
+
st.error("3D λ λλ§μ μ€ν¨νμ΅λλ€.")
|
| 3167 |
|
| 3168 |
+
st.caption("π‘ 3D μ
체λλ μλ²μμ λ λλ§λμ΄ WebGL μμ΄ νμλ©λλ€.")
|
| 3169 |
|
| 3170 |
# Educational Description
|
| 3171 |
descriptions = {
|
|
|
|
| 3226 |
anim_func = ANIMATED_LANDFORM_GENERATORS[landform_key]
|
| 3227 |
stage_elev = anim_func(gallery_grid_size, stage_value)
|
| 3228 |
|
| 3229 |
+
# 2D/3D ν κΈ (μλ²μ¬μ΄λ λ λλ§)
|
| 3230 |
+
anim_view_mode = st.radio("보기 λͺ¨λ", ["2D νλ©΄λ", "3D μ
체λ"], horizontal=True, key="anim_view_mode")
|
|
|
|
| 3231 |
|
| 3232 |
+
if anim_view_mode == "2D νλ©΄λ":
|
| 3233 |
+
# 2D matplotlib
|
| 3234 |
+
fig_2d, ax_2d = plt.subplots(figsize=(10, 8))
|
| 3235 |
+
im = ax_2d.imshow(stage_elev, cmap='terrain', origin='upper')
|
| 3236 |
+
water_mask = stage_elev < 0
|
| 3237 |
+
if water_mask.any():
|
| 3238 |
+
water_overlay = np.ma.masked_where(~water_mask, np.ones_like(stage_elev))
|
| 3239 |
+
ax_2d.imshow(water_overlay, cmap='Blues', alpha=0.6, origin='upper')
|
| 3240 |
+
ax_2d.set_title(f"{selected_landform} - {int(stage_value*100)}%", fontsize=14)
|
| 3241 |
+
ax_2d.axis('off')
|
| 3242 |
+
plt.colorbar(im, ax=ax_2d, shrink=0.6, label='κ³ λ (m)')
|
| 3243 |
+
st.pyplot(fig_2d)
|
| 3244 |
+
plt.close(fig_2d)
|
| 3245 |
+
else:
|
| 3246 |
+
# 3D μλ²μ¬μ΄λ λ λλ§ (WebGL μ¬μ© μ ν¨!)
|
| 3247 |
+
with st.spinner("3D λ λλ§ μ€..."):
|
| 3248 |
+
stage_water = np.maximum(0, -stage_elev + 1.0)
|
| 3249 |
+
stage_water[stage_elev > 2] = 0
|
| 3250 |
+
fig_3d = render_terrain_plotly(
|
| 3251 |
+
stage_elev,
|
| 3252 |
+
f"{selected_landform} - {int(stage_value*100)}%",
|
| 3253 |
+
add_water=True,
|
| 3254 |
+
water_depth_grid=stage_water,
|
| 3255 |
+
water_level=-999,
|
| 3256 |
+
force_camera=True
|
| 3257 |
+
)
|
| 3258 |
+
img_bytes = render_3d_as_image(fig_3d, width=800, height=600)
|
| 3259 |
+
if img_bytes:
|
| 3260 |
+
st.image(img_bytes, caption=f"{selected_landform} - {int(stage_value*100)}% (3D)", use_column_width=True)
|
| 3261 |
+
else:
|
| 3262 |
+
st.error("3D λ λλ§μ μ€ν¨νμ΅λλ€.")
|
| 3263 |
|
| 3264 |
st.caption("π‘ μ¬λΌμ΄λλ₯Ό μ‘°μ νμ¬ νμ± λ¨κ³λ₯Ό νμΈνμΈμ. (0% = μμ, 100% = μμ±)")
|
| 3265 |
|
requirements.txt
CHANGED
|
@@ -4,3 +4,4 @@ plotly>=5.18.0
|
|
| 4 |
matplotlib>=3.7.0
|
| 5 |
scipy>=1.11.0
|
| 6 |
Pillow>=10.0.0
|
|
|
|
|
|
| 4 |
matplotlib>=3.7.0
|
| 5 |
scipy>=1.11.0
|
| 6 |
Pillow>=10.0.0
|
| 7 |
+
kaleido>=0.2.1
|