Upload 2 files
Browse files- app.py +171 -0
- requirements.txt +3 -0
app.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# =======================================================================
|
| 2 |
+
# ШАГ 1: Импорт библиотек
|
| 3 |
+
# =======================================================================
|
| 4 |
+
import os
|
| 5 |
+
import json
|
| 6 |
+
import ee
|
| 7 |
+
import geemap
|
| 8 |
+
import gradio as gr
|
| 9 |
+
import html
|
| 10 |
+
|
| 11 |
+
# =======================================================================
|
| 12 |
+
# ШАГ 2: Аутентификация и инициализация GEE (АДАПТИРОВАНО ДЛЯ HF SPACES)
|
| 13 |
+
# =======================================================================
|
| 14 |
+
print("\n--- Инициализация Google Earth Engine ---")
|
| 15 |
+
try:
|
| 16 |
+
# Проверяем, запущено ли приложение в Hugging Face Spaces и есть ли секрет
|
| 17 |
+
if 'GEE_CREDENTIALS' in os.environ:
|
| 18 |
+
print("Аутентификация через секрет Hugging Face...")
|
| 19 |
+
# Декодируем JSON-строку из секрета
|
| 20 |
+
creds_json_str = os.environ['GEE_CREDENTIALS']
|
| 21 |
+
creds_json = json.loads(creds_json_str)
|
| 22 |
+
|
| 23 |
+
# Создаем учетные данные и инициализируем GEE
|
| 24 |
+
credentials = ee.ServiceAccountCredentials(creds_json['client_email'], key_data=creds_json_str)
|
| 25 |
+
ee.Initialize(credentials=credentials, project=creds_json['project_id'])
|
| 26 |
+
print("✅ Аутентификация через сервисный аккаунт GEE прошла успешно.")
|
| 27 |
+
else:
|
| 28 |
+
# Резервный вариант для локального запуска (если секрета нет)
|
| 29 |
+
print("Секрет не найден, попытка локальной аутентификации...")
|
| 30 |
+
ee.Authenticate()
|
| 31 |
+
ee.Initialize(project='gen-lang-client-0605302377')
|
| 32 |
+
print("✅ Локальная аутентификация и инициализация GEE прошли успешно.")
|
| 33 |
+
|
| 34 |
+
except Exception as e:
|
| 35 |
+
print(f"🔴 Критическая ошибка на этапе инициализации GEE: {e}")
|
| 36 |
+
# В среде HF это приведет к ошибке приложения, что и требуется.
|
| 37 |
+
|
| 38 |
+
# =======================================================================
|
| 39 |
+
# ШАГ 3: Обучение модели-классификатора
|
| 40 |
+
# (Код остается без изменений)
|
| 41 |
+
# =======================================================================
|
| 42 |
+
gee_classifier = None
|
| 43 |
+
bands_for_training = ['B2', 'B3', 'B4', 'B8', 'NDVI']
|
| 44 |
+
|
| 45 |
+
def add_ndvi(image):
|
| 46 |
+
ndvi = image.normalizedDifference(['B8', 'B4']).rename('NDVI')
|
| 47 |
+
return image.addBands(ndvi)
|
| 48 |
+
|
| 49 |
+
def train_classifier():
|
| 50 |
+
global gee_classifier
|
| 51 |
+
if gee_classifier:
|
| 52 |
+
print("✅ Классификатор уже обучен.")
|
| 53 |
+
return
|
| 54 |
+
|
| 55 |
+
print("⏳ Обучение классификатора GEE... Это может занять около минуты.")
|
| 56 |
+
try:
|
| 57 |
+
desert = ee.FeatureCollection('projects/gen-lang-client-0605302377/assets/kalmykia_desert_samples').map(lambda f: f.set('class', 0))
|
| 58 |
+
solonchak = ee.FeatureCollection('projects/gen-lang-client-0605302377/assets/kalmykia_solonchak_samples').map(lambda f: f.set('class', 1))
|
| 59 |
+
arid = ee.FeatureCollection('projects/gen-lang-client-0605302377/assets/kalmykia_arid_samples').map(lambda f: f.set('class', 2))
|
| 60 |
+
greenery = ee.FeatureCollection('projects/gen-lang-client-0605302377/assets/kalmykia_greenery_samples').map(lambda f: f.set('class', 3))
|
| 61 |
+
water = ee.FeatureCollection('projects/gen-lang-client-0605302377/assets/kalmykia_water_samples').map(lambda f: f.set('class', 4))
|
| 62 |
+
|
| 63 |
+
training_points = desert.merge(solonchak).merge(arid).merge(greenery).merge(water)
|
| 64 |
+
roi_for_training = training_points.geometry().bounds()
|
| 65 |
+
|
| 66 |
+
image_for_training = ee.ImageCollection('COPERNICUS/S2_SR_HARMONIZED') \
|
| 67 |
+
.filterDate('2023-06-01', '2023-09-01') \
|
| 68 |
+
.filterBounds(roi_for_training) \
|
| 69 |
+
.filter(ee.Filter.lt('CLOUDY_PIXEL_PERCENTAGE', 10)) \
|
| 70 |
+
.map(add_ndvi) \
|
| 71 |
+
.median() \
|
| 72 |
+
.clip(roi_for_training)
|
| 73 |
+
|
| 74 |
+
training_data = image_for_training.select(bands_for_training).sampleRegions(
|
| 75 |
+
collection=training_points, properties=['class'], scale=10, tileScale=4)
|
| 76 |
+
|
| 77 |
+
gee_classifier = ee.Classifier.smileRandomForest(numberOfTrees=50).train(
|
| 78 |
+
features=training_data, classProperty='class', inputProperties=bands_for_training)
|
| 79 |
+
print("✅ Модель-классификатор успешно обучена на 5 классах!")
|
| 80 |
+
except Exception as e:
|
| 81 |
+
print(f"🔴 Критическая ошибка во время обучения модели: {e}")
|
| 82 |
+
|
| 83 |
+
train_classifier()
|
| 84 |
+
|
| 85 |
+
# =======================================================================
|
| 86 |
+
# ШАГ 4: Функции для работы Gradio-приложения
|
| 87 |
+
# (Код остается без ��зменений)
|
| 88 |
+
# =======================================================================
|
| 89 |
+
def get_region_info(region_name):
|
| 90 |
+
regions = {
|
| 91 |
+
"Элиста (тестовая область)": {"center": [46.307, 44.258], "bbox": [44.15, 46.25, 44.35, 46.35]},
|
| 92 |
+
"Озеро Сарпа (Калмыкия)": {"center": [47.85, 44.75], "bbox": [44.65, 47.80, 44.85, 47.90]},
|
| 93 |
+
"Арзгир (Ставроп. край)": {"center": [45.38, 44.22], "bbox": [44.12, 45.33, 44.32, 45.43]},
|
| 94 |
+
"Будённовск (Ставроп. край)": {"center": [44.78, 44.15], "bbox": [44.05, 44.73, 44.25, 44.83]},
|
| 95 |
+
"Москва (тестовая область)": {"center": [55.75, 37.61], "bbox": [37.5, 55.7, 37.7, 55.8]}
|
| 96 |
+
}
|
| 97 |
+
return regions.get(region_name)
|
| 98 |
+
|
| 99 |
+
def generate_classified_map(region_info, year, classifier):
|
| 100 |
+
if not classifier: return None, "Ошибка: Классификатор не обучен."
|
| 101 |
+
print(f"-- Запрос на генерацию карты для {year} года...")
|
| 102 |
+
roi_to_classify = ee.Geometry.Rectangle(region_info['bbox'])
|
| 103 |
+
collection = ee.ImageCollection('COPERNICUS/S2_SR_HARMONIZED') \
|
| 104 |
+
.filterDate(f'{year}-06-01', f'{year}-09-01') \
|
| 105 |
+
.filterBounds(roi_to_classify) \
|
| 106 |
+
.filter(ee.Filter.lt('CLOUDY_PIXEL_PERCENTAGE', 25))
|
| 107 |
+
if collection.size().getInfo() == 0: return None, f"⚠️ Не найдено снимков за {year} год."
|
| 108 |
+
image_to_classify = collection.map(add_ndvi).median().clip(roi_to_classify)
|
| 109 |
+
classified_image = image_to_classify.classify(classifier)
|
| 110 |
+
class_palette = ['#e3a25a', '#ffffff', '#ffff00', '#00ff00', '#0000FF']
|
| 111 |
+
vis_params = {'min': 0, 'max': 4, 'palette': class_palette}
|
| 112 |
+
map_id = classified_image.getMapId(vis_params)
|
| 113 |
+
print(f"-- ✅ Карта для {year} года сгенерирована.")
|
| 114 |
+
return {"center": region_info['center'], "tile_url": map_id['tile_fetcher'].url_format, "year": year}, None
|
| 115 |
+
|
| 116 |
+
def create_map_iframe_html(map_data, region_name):
|
| 117 |
+
center, tile_url, year = map_data['center'], map_data['tile_url'], map_data['year']
|
| 118 |
+
iframe_content = f"""
|
| 119 |
+
<!DOCTYPE html><html><head><title>Карта {year}</title><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0"><link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" /><script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"></script><style>html, body, #map {{ height: 100%; width: 100%; margin: 0; padding: 0; }}</style></head><body><div id="map"></div><script>
|
| 120 |
+
var map = L.map('map').setView([{center[0]}, {center[1]}], 12);
|
| 121 |
+
L.tileLayer('https://{{s}}.tile.openstreetmap.org/{{z}}/{{x}}/{{y}}.png', {{ attribution: '© OpenStreetMap' }}).addTo(map);
|
| 122 |
+
L.tileLayer(`{tile_url}`, {{ attribution: 'Google Earth Engine' }}).addTo(map);
|
| 123 |
+
</script></body></html>"""
|
| 124 |
+
escaped_html = html.escape(iframe_content)
|
| 125 |
+
return f'<iframe srcdoc="{escaped_html}" style="width: 100%; height: 500px; border: 1px solid #ccc;"></iframe>'
|
| 126 |
+
|
| 127 |
+
def process_and_display_maps(region_name, year1, year2, year3):
|
| 128 |
+
if not gee_classifier: return "Ошибка: модель не обучена.", "", ""
|
| 129 |
+
region_info = get_region_info(region_name)
|
| 130 |
+
years = sorted(list(set([year1, year2, year3])))
|
| 131 |
+
outputs, messages = [], []
|
| 132 |
+
print(f"\n🚀 Новый запрос! Регион: {region_name}, Годы: {years}")
|
| 133 |
+
for year in years:
|
| 134 |
+
map_data, msg = generate_classified_map(region_info, year, gee_classifier)
|
| 135 |
+
if map_data: outputs.append(create_map_iframe_html(map_data, region_name))
|
| 136 |
+
else: outputs.append(f"<p style='text-align:center; padding-top: 200px;'>{msg}</p>")
|
| 137 |
+
if msg: messages.append(msg)
|
| 138 |
+
while len(outputs) < 3: outputs.append(None)
|
| 139 |
+
final_message = "✅ Готово. " + " ".join(messages)
|
| 140 |
+
return outputs[0], outputs[1], outputs[2], final_message
|
| 141 |
+
|
| 142 |
+
# =======================================================================
|
| 143 |
+
# ШАГ 5: Создание и запуск интерфейса Gradio
|
| 144 |
+
# (Код остается почти без изменений)
|
| 145 |
+
# =======================================================================
|
| 146 |
+
with gr.Blocks(css=".gradio-container {max-width: 1200px !important;}") as demo:
|
| 147 |
+
gr.Markdown("# 🛰️ Анализ почвенного покрова")
|
| 148 |
+
gr.Markdown("Выберите регион и три года для анализа. Карты появятся ниже.")
|
| 149 |
+
with gr.Row():
|
| 150 |
+
with gr.Column(scale=1):
|
| 151 |
+
region_dropdown = gr.Dropdown(
|
| 152 |
+
label="Регион",
|
| 153 |
+
choices=["Элиста (тестовая область)", "Озеро Сарпа (Калмыкия)", "Арзгир (Ставроп. край)", "Будённовск (Ставроп. край)", "Москва (тестовая область)"],
|
| 154 |
+
value="Элиста (тестовая область)")
|
| 155 |
+
year1_slider = gr.Slider(label="Год 1", minimum=2019, maximum=2025, step=1, value=2019)
|
| 156 |
+
year2_slider = gr.Slider(label="Год 2", minimum=2019, maximum=2025, step=1, value=2021)
|
| 157 |
+
year3_slider = gr.Slider(label="Год 3", minimum=2019, maximum=2025, step=1, value=2023)
|
| 158 |
+
submit_button = gr.Button("Сгенерировать карты", variant="primary")
|
| 159 |
+
status_message = gr.Markdown()
|
| 160 |
+
with gr.Row():
|
| 161 |
+
map1_output = gr.HTML()
|
| 162 |
+
map2_output = gr.HTML()
|
| 163 |
+
map3_output = gr.HTML()
|
| 164 |
+
submit_button.click(
|
| 165 |
+
fn=process_and_display_maps,
|
| 166 |
+
inputs=[region_dropdown, year1_slider, year2_slider, year3_slider],
|
| 167 |
+
outputs=[map1_output, map2_output, map3_output, status_message])
|
| 168 |
+
|
| 169 |
+
# --- ЗАПУСК ПРИЛОЖЕНИЯ ---
|
| 170 |
+
print("\n--- Запуск Gradio интерфейса ---")
|
| 171 |
+
demo.launch()
|
requirements.txt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio
|
| 2 |
+
geemap
|
| 3 |
+
earthengine-api
|