我们开发了一个 Web 应用程序原型,该原型利用大型语言模型 (LLM) 和 Google Earth Engine (GEE) 来简化卫星图像和土地利用/土地覆盖 (LULC) 地图的可视化。尽管卫星图像对于环境监测、农业和城市规划至关重要,但大多数最终用户发现访问和绘制卫星图像的过程颇具挑战性。因此,提供一个直观的界面,让用户能够通过自然语言聊天轻松请求卫星可视化,可以极大地提升可访问性和易用性。
先决条件
为了实现这一目标,您需要 OpenAI API 密钥和 Google Earth Engine API 密钥,才能运行 ChatGPT 并通过 GEE 访问地理空间数据。
此外,我们使用 LangChain 管理与 LLM 的交互,并使用 Streamlit 构建基于 Web 的用户界面。
方法
LLM申请的工作流程
我们的基于 LLM 的可视化系统的工作流程包括四个主要步骤:
用户输入:
用户通过聊天界面与系统自然互动,以纯文本形式提出请求,例如“显示 2024 年 1 月至 3 月期间东京的图像”。
使用 GPT 进行文本处理:
GPT 模型处理用户的输入,以准确识别和提取关键参数,特别是地理位置和时间持续时间。此步骤涉及将自然语言查询解析为结构化参数。
通过 GEE 收集卫星图像:
该应用程序使用 GPT 提取的结构化参数,查询 Google Earth Engine 以检索与用户指定位置和时间范围相关的 Sentinel 卫星图像和相关的 Dynamic World 土地利用分类数据。
调用函数来可视化收集的数据:
检索到的卫星图像和分类地图通过预定义的 Python 函数进行可视化,并在 OpenStreetMap (OSM) 上以交互方式显示结果,使用户能够无缝地探索空间和时间方面。
全部代码如下:
import os
import streamlit as st
import ee
import geemap.foliumap as geemap
from langchain_openai import ChatOpenAI
from langchain.schema import StrOutputParser, HumanMessage
from langchain_core.prompts import PromptTemplate
from langchain_core.caches import InMemoryCache
from langchain_core.globals import set_llm_cache
import ast
class VisualizationAssistant:
# class‐level constants
CLASS_NAMES = [
'water', 'trees', 'grass', 'flooded_vegetation',
'crops', 'shrub_and_scrub', 'built', 'bare', 'snow_and_ice'
]
VIS_PALETTE = [
'419bdf', '397d49', '88b053', '7a87c6', 'e49635',
'dfc35a', 'c4281b', 'a59b8f', 'b39fe1'
]
def __init__(self, model: ChatOpenAI):
self.model = model
def extract_data(self, question: str) -> dict:
prompt = f"""
You are a data extraction agent specialized in geospatial and temporal analysis.
Given a user's query about a location and time period, extract:
- The exact geographic point as [longitude, latitude] in decimal degrees.
- The START and END dates as strings in 'YYYY-MM-DD' format.
Rules:
- If the user specifies only a year, set START = 'YYYY-01-01' and END = 'YYYY-12-31'.
- If the user specifies vague dates like seasons or months, pick reasonable date ranges (e.g. August to September).
- If the user specifies a place name (city, country, region), return the coordinates of its central or capital location.
Output **only** a single JSON dictionary like this (no extra text):
{{"point": [longitude, latitude], "START": "YYYY-MM-DD", "END": "YYYY-MM-DD"}}
User query: "{question}"
"""
messages = [HumanMessage(content=prompt)]
return self.model.invoke(messages).content.strip()
def create_dw_rgb_hillshade(self, image: ee.Image) -> ee.Image:
label_rgb = image.select('label') \
.visualize(min=0, max=8, palette=self.VIS_PALETTE) \
.divide(255)
prob_max = image.select(self.CLASS_NAMES).reduce(ee.Reducer.max())
hillshade = ee.Terrain.hillshade(prob_max.multiply(100)).divide(255)
return label_rgb.multiply(hillshade) \
.set('system:time_start', image.get('system:time_start'))
def add_visual_layers(self, dw_vis: ee.Image, s2_img: ee.Image, m: geemap.Map):
date = ee.Date(dw_vis.get('system:time_start')).format('YYYY-MM-dd').getInfo()
m.addLayer(s2_img, {'min': 0, 'max': 3000, 'bands': ['B4','B3','B2']},
f"RGB {date}")
m.addLayer(dw_vis, {'min': 0, 'max': 0.65}, f"DW {date}")
def show(self, point_coords: list, START: str, END: str):
point = ee.Geometry.Point(point_coords)
filt = ee.Filter.And(ee.Filter.bounds(point), ee.Filter.date(START, END))
dw_col = ee.ImageCollection('GOOGLE/DYNAMICWORLD/V1').filter(filt)
s2_col = ee.ImageCollection('COPERNICUS/S2_HARMONIZED').filter(filt)
s2_names = s2_col.first().bandNames()
linked = dw_col.linkCollection(s2_col, s2_names)
dw_vis_col = linked.map(lambda img: self.create_dw_rgb_hillshade(img))
# build map
m = geemap.Map(center=[point_coords[1], point_coords[0]], zoom=12)
size = linked.size().getInfo()
for i in range(size):
img = ee.Image(linked.toList(size).get(i))
dw_vis = self.create_dw_rgb_hillshade(img)
self.add_visual_layers(dw_vis, img, m)
# legend
legend = {name: f"#{col}" for name, col in zip(self.CLASS_NAMES, self.VIS_PALETTE)}
m.add_legend(title="Dynamic World Classes", legend_dict=legend)
st.title("🌍 Dynamic World overlaid on Sentinel-2")
m.to_streamlit(height=600)
def initialize_session_state():
if "messages" not in st.session_state:
st.session_state.messages = [
{"role": "assistant",
"content": "👋 Hi! Enter the area and duration you want to plot"}
]
def main():
st.set_page_config(page_title="Dynamic World Visualizer", layout="wide")
initialize_session_state()
ee.Authenticate()
ee.Initialize(project=os.getenv("YOUR_GEE_PROJECT_ID"))
# LLM setup
model = ChatOpenAI(
model="gpt-4o-mini", temperature=0,
max_tokens=10000, timeout=30000,
verbose=True, api_key=os.getenv("YOUR_OPENAI_API_KEY")
)
assistant = VisualizationAssistant(model)
# render chat history
for msg in st.session_state.messages:
with st.chat_message(msg["role"]):
st.write(msg["content"])
# input
if prompt := st.chat_input("Ex: 'Tokyo, May to August 2023'"):
st.session_state.messages.append({"role":"user","content":prompt})
st.chat_message("user").write(prompt)
# extract and visualize
raw = assistant.extract_data(prompt)
with st.chat_message("assistant"):
st.write(f"🔍 Extracted result: `{raw}`")
spec = ast.literal_eval(raw)
point = spec["point"]
point = list(point)
assistant.show(point, spec["START"], spec["END"])
st.session_state.messages.append({"role":"assistant","content":spec})
if __name__ == "__main__":
if 'initialized' not in st.session_state:
set_llm_cache(InMemoryCache())
st.session_state.initialized = True
main()
结果
下图展示了结果。输入问题后,提取的位置和持续时间会显示出来。随后,它会以RGB格式提供哨兵数据的时间序列可视化,并与指定时间段内的动态世界地图叠加。这样可以轻松识别城市发展、森林变化或农作物种植面积的变化等。
聊天显示
可视化结果
概括
该原型展示了通过对话式人工智能提升卫星图像可访问性和可用性的巨大潜力。将 LLM 与 Google Earth Engine 等专业的地理空间数据 API 集成,可以使复杂的空间分析和可视化变得用户友好,并让非专业人士也能轻松上手。展望未来,此类应用有望通过简化环境管理、灾害响应、农业监测和城市发展规划等各个领域的数据驱动决策,对遥感领域产生重大影响。