HANSOL commited on
Commit
d7747e3
Β·
1 Parent(s): b2b4324

Server-side 3D rendering with kaleido - no WebGL needed

Browse files
Files changed (2) hide show
  1. app/main.py +78 -41
  2. 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
- # κΈ°λ³Έ: 2D 평면도 (matplotlib) - WebGL μ»¨ν…μŠ€νŠΈ μ‚¬μš© μ•ˆ 함
3123
- import matplotlib.pyplot as plt
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
- st.pyplot(fig_2d)
3148
- plt.close(fig_2d)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3149
 
3150
- st.caption("πŸ’‘ 2D ν‰λ©΄λ„λ‘œ μ§€ν˜•μ„ ν™•μΈν•˜μ„Έμš”. μ•„λž˜ ν˜•μ„± κ³Όμ •μ—μ„œ μŠ¬λΌμ΄λ”λ‘œ λ³€ν™”λ₯Ό κ΄€μ°°ν•  수 μžˆμŠ΅λ‹ˆλ‹€.")
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 matplotlib만 μ‚¬μš© (WebGL 문제 λ°©μ§€)
3212
- fig_2d, ax_2d = plt.subplots(figsize=(10, 8))
3213
- im = ax_2d.imshow(stage_elev, cmap='terrain', origin='upper')
3214
 
3215
- # λ¬Ό μ˜μ—­
3216
- water_mask = stage_elev < 0
3217
- if water_mask.any():
3218
- water_overlay = np.ma.masked_where(~water_mask, np.ones_like(stage_elev))
3219
- ax_2d.imshow(water_overlay, cmap='Blues', alpha=0.6, origin='upper')
3220
-
3221
- ax_2d.set_title(f"{selected_landform} - {int(stage_value*100)}%", fontsize=14)
3222
- ax_2d.axis('off')
3223
- plt.colorbar(im, ax=ax_2d, shrink=0.6, label='고도 (m)')
3224
- st.pyplot(fig_2d)
3225
- plt.close(fig_2d)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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