cd_prediction.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575
  1. """
  2. Cd预测模型API接口
  3. @description: 提供作物Cd和有效态Cd的预测与可视化功能
  4. """
  5. from fastapi import APIRouter, HTTPException, BackgroundTasks, UploadFile, File, Form
  6. from fastapi.responses import FileResponse
  7. from typing import Dict, Any, Optional, List
  8. import os
  9. import logging
  10. import io
  11. import pandas as pd
  12. from ..services.cd_prediction_service_v3 import CdPredictionServiceV3
  13. from ..services.cd_prediction_database_service import CdPredictionDatabaseService
  14. router = APIRouter()
  15. # 设置日志
  16. logger = logging.getLogger(__name__)
  17. # =============================================================================
  18. # 一键生成并获取地图接口
  19. # =============================================================================
  20. @router.post("/crop-cd/generate-and-get-map",
  21. summary="一键生成并获取作物Cd预测地图",
  22. description="根据区域名称和行政级别生成作物Cd预测地图并直接返回图片文件,优先使用数据库数据,也支持CSV文件上传")
  23. async def generate_and_get_crop_cd_map(
  24. area: str = Form(..., description="区域名称,如:乐昌市"),
  25. level: str = Form("auto", description="行政级别,如:county, city, province,默认为auto自动识别"),
  26. use_database: Optional[bool] = Form(True, description="是否使用数据库数据,默认为True"),
  27. data_file: Optional[UploadFile] = File(None, description="可选的CSV格式环境因子数据文件,仅在use_database=False时使用"),
  28. enable_interpolation: Optional[bool] = Form(None, description="是否启用空间插值"),
  29. interpolation_method: Optional[str] = Form(None, description="插值方法: nearest, linear, cubic"),
  30. resolution_factor: Optional[float] = Form(None, description="分辨率因子,越大分辨率越高")
  31. ):
  32. """
  33. 一键生成并获取作物Cd预测地图
  34. @param area: 区域名称
  35. @param level: 行政级别
  36. @param use_database: 是否使用数据库数据
  37. @param data_file: 可选的CSV数据文件,仅在use_database=False时使用
  38. @returns {FileResponse} 预测地图文件
  39. """
  40. try:
  41. logger.info(f"开始为{area}({level})一键生成作物Cd预测地图(数据源:{'数据库' if use_database else 'CSV文件'})")
  42. # 构建栅格配置参数
  43. raster_params = {}
  44. if enable_interpolation is not None:
  45. raster_params['enable_interpolation'] = enable_interpolation
  46. if interpolation_method is not None:
  47. raster_params['interpolation_method'] = interpolation_method
  48. if resolution_factor is not None:
  49. raster_params['resolution_factor'] = resolution_factor
  50. if use_database:
  51. # 使用数据库数据生成预测
  52. db_service = CdPredictionDatabaseService()
  53. result = await db_service.generate_crop_cd_prediction_from_database(
  54. area=area,
  55. level=level,
  56. raster_config_override=raster_params if raster_params else None
  57. )
  58. if not result['map_path'] or not os.path.exists(result['map_path']):
  59. raise HTTPException(status_code=500, detail="基于数据库数据的地图文件生成失败")
  60. logger.info(f"使用数据库数据为{area}({level})生成作物Cd预测地图成功,处理{result.get('processed_records', 0)}条记录")
  61. else:
  62. # 使用上传的CSV文件生成预测
  63. if not data_file:
  64. raise HTTPException(status_code=400, detail="当use_database=False时,必须提供data_file")
  65. # 验证文件格式
  66. if not data_file.filename.endswith('.csv'):
  67. raise HTTPException(status_code=400, detail="仅支持CSV格式文件")
  68. # 读取CSV数据
  69. content = await data_file.read()
  70. df = pd.read_csv(io.StringIO(content.decode('utf-8')))
  71. # 验证数据格式
  72. if df.shape[1] < 3:
  73. raise HTTPException(
  74. status_code=400,
  75. detail="数据至少需要3列:前两列为经纬度,后续列为环境因子"
  76. )
  77. # 重命名前两列为标准的经纬度列名
  78. df.columns = ['longitude', 'latitude'] + list(df.columns[2:])
  79. service = CdPredictionServiceV3()
  80. # 验证数据
  81. validation_result = service.validate_input_data(df, area)
  82. if not validation_result['valid']:
  83. raise HTTPException(
  84. status_code=400,
  85. detail=f"数据验证失败: {', '.join(validation_result['errors'])}"
  86. )
  87. # 保存临时数据文件
  88. temp_file_path = service.save_temp_data(df, area)
  89. # 生成预测结果
  90. result = await service.generate_crop_cd_prediction_for_county(
  91. county_name=area,
  92. data_file=temp_file_path,
  93. raster_config_override=raster_params if raster_params else None
  94. )
  95. if not result['map_path'] or not os.path.exists(result['map_path']):
  96. raise HTTPException(status_code=500, detail="基于CSV文件的地图文件生成失败")
  97. logger.info(f"使用CSV文件为{area}({level})生成作物Cd预测地图成功")
  98. return FileResponse(
  99. path=result['map_path'],
  100. filename=f"{area}_crop_cd_prediction_map.jpg",
  101. media_type="image/jpeg"
  102. )
  103. except HTTPException:
  104. raise
  105. except Exception as e:
  106. logger.error(f"为{area}({level})一键生成作物Cd预测地图失败: {str(e)}")
  107. raise HTTPException(
  108. status_code=500,
  109. detail=f"为{area}({level})一键生成作物Cd预测地图失败: {str(e)}"
  110. )
  111. @router.post("/crop-cd/generate-from-database",
  112. summary="基于数据库数据生成作物Cd预测地图",
  113. description="直接从数据库读取数据生成作物Cd预测地图")
  114. async def generate_crop_cd_map_from_database(
  115. area: str = Form(..., description="区域名称,如:乐昌市"),
  116. level: str = Form("auto", description="行政级别,如:county, city, province,默认为auto自动识别"),
  117. enable_interpolation: Optional[bool] = Form(None, description="是否启用空间插值"),
  118. interpolation_method: Optional[str] = Form(None, description="插值方法: nearest, linear, cubic"),
  119. resolution_factor: Optional[float] = Form(None, description="分辨率因子,越大分辨率越高")
  120. ):
  121. """
  122. 基于数据库数据生成作物Cd预测地图
  123. @param area: 区域名称
  124. @param level: 行政级别
  125. @returns {FileResponse} 预测地图文件
  126. """
  127. try:
  128. logger.info(f"开始基于数据库数据为{area}({level})生成作物Cd预测地图")
  129. # 构建栅格配置参数
  130. raster_params = {}
  131. if enable_interpolation is not None:
  132. raster_params['enable_interpolation'] = enable_interpolation
  133. if interpolation_method is not None:
  134. raster_params['interpolation_method'] = interpolation_method
  135. if resolution_factor is not None:
  136. raster_params['resolution_factor'] = resolution_factor
  137. # 使用数据库数据生成预测
  138. db_service = CdPredictionDatabaseService()
  139. result = await db_service.generate_crop_cd_prediction_from_database(
  140. area=area,
  141. level=level,
  142. raster_config_override=raster_params if raster_params else None
  143. )
  144. if not result['map_path'] or not os.path.exists(result['map_path']):
  145. raise HTTPException(status_code=500, detail="基于数据库数据的地图文件生成失败")
  146. logger.info(f"基于数据库数据为{area}({level})生成作物Cd预测地图成功,处理{result.get('processed_records', 0)}条记录")
  147. return FileResponse(
  148. path=result['map_path'],
  149. filename=f"{area}_crop_cd_prediction_map_database.jpg",
  150. media_type="image/jpeg"
  151. )
  152. except HTTPException:
  153. raise
  154. except Exception as e:
  155. logger.error(f"基于数据库数据为{area}({level})生成作物Cd预测地图失败: {str(e)}")
  156. raise HTTPException(
  157. status_code=500,
  158. detail=f"基于数据库数据为{area}({level})生成作物Cd预测地图失败: {str(e)}"
  159. )
  160. @router.post("/effective-cd/generate-and-get-map",
  161. summary="一键生成并获取有效态Cd预测地图",
  162. description="根据区域名称和行政级别生成有效态Cd预测地图并直接返回图片文件,优先使用数据库数据,也支持CSV文件上传")
  163. async def generate_and_get_effective_cd_map(
  164. area: str = Form(..., description="区域名称,如:乐昌市"),
  165. level: str = Form("auto", description="行政级别,如:county, city, province,默认为auto自动识别"),
  166. use_database: Optional[bool] = Form(True, description="是否使用数据库数据,默认为True"),
  167. data_file: Optional[UploadFile] = File(None, description="可选的CSV格式环境因子数据文件,仅在use_database=False时使用"),
  168. enable_interpolation: Optional[bool] = Form(None, description="是否启用空间插值"),
  169. interpolation_method: Optional[str] = Form(None, description="插值方法: nearest, linear, cubic"),
  170. resolution_factor: Optional[float] = Form(None, description="分辨率因子,越大分辨率越高")
  171. ):
  172. """
  173. 一键生成并获取有效态Cd预测地图
  174. @param area: 区域名称
  175. @param level: 行政级别
  176. @param use_database: 是否使用数据库数据
  177. @param data_file: 可选的CSV数据文件,仅在use_database=False时使用
  178. @returns {FileResponse} 预测地图文件
  179. """
  180. try:
  181. logger.info(f"开始为{area}({level})一键生成有效态Cd预测地图(数据源:{'数据库' if use_database else 'CSV文件'})")
  182. # 构建栅格配置参数
  183. raster_params = {}
  184. if enable_interpolation is not None:
  185. raster_params['enable_interpolation'] = enable_interpolation
  186. if interpolation_method is not None:
  187. raster_params['interpolation_method'] = interpolation_method
  188. if resolution_factor is not None:
  189. raster_params['resolution_factor'] = resolution_factor
  190. if use_database:
  191. # 使用数据库数据生成预测
  192. db_service = CdPredictionDatabaseService()
  193. result = await db_service.generate_effective_cd_prediction_from_database(
  194. area=area,
  195. level=level,
  196. raster_config_override=raster_params if raster_params else None
  197. )
  198. if not result['map_path'] or not os.path.exists(result['map_path']):
  199. raise HTTPException(status_code=500, detail="基于数据库数据的地图文件生成失败")
  200. logger.info(f"使用数据库数据为{area}({level})生成有效态Cd预测地图成功,处理{result.get('processed_records', 0)}条记录")
  201. else:
  202. # 使用上传的CSV文件生成预测
  203. if not data_file:
  204. raise HTTPException(status_code=400, detail="当use_database=False时,必须提供data_file")
  205. # 验证文件格式
  206. if not data_file.filename.endswith('.csv'):
  207. raise HTTPException(status_code=400, detail="仅支持CSV格式文件")
  208. # 读取CSV数据
  209. content = await data_file.read()
  210. df = pd.read_csv(io.StringIO(content.decode('utf-8')))
  211. # 验证数据格式
  212. if df.shape[1] < 3:
  213. raise HTTPException(
  214. status_code=400,
  215. detail="数据至少需要3列:前两列为经纬度,后续列为环境因子"
  216. )
  217. # 重命名前两列为标准的经纬度列名
  218. df.columns = ['longitude', 'latitude'] + list(df.columns[2:])
  219. service = CdPredictionServiceV3()
  220. # 验证数据
  221. validation_result = service.validate_input_data(df, area)
  222. if not validation_result['valid']:
  223. raise HTTPException(
  224. status_code=400,
  225. detail=f"数据验证失败: {', '.join(validation_result['errors'])}"
  226. )
  227. # 保存临时数据文件
  228. temp_file_path = service.save_temp_data(df, area)
  229. # 生成预测结果
  230. result = await service.generate_effective_cd_prediction_for_county(
  231. county_name=area,
  232. data_file=temp_file_path,
  233. raster_config_override=raster_params if raster_params else None
  234. )
  235. if not result['map_path'] or not os.path.exists(result['map_path']):
  236. raise HTTPException(status_code=500, detail="基于CSV文件的地图文件生成失败")
  237. logger.info(f"使用CSV文件为{area}({level})生成有效态Cd预测地图成功")
  238. return FileResponse(
  239. path=result['map_path'],
  240. filename=f"{area}_effective_cd_prediction_map.jpg",
  241. media_type="image/jpeg"
  242. )
  243. except HTTPException:
  244. raise
  245. except Exception as e:
  246. logger.error(f"为{area}({level})一键生成有效态Cd预测地图失败: {str(e)}")
  247. raise HTTPException(
  248. status_code=500,
  249. detail=f"为{area}({level})一键生成有效态Cd预测地图失败: {str(e)}"
  250. )
  251. @router.post("/effective-cd/generate-from-database",
  252. summary="基于数据库数据生成有效态Cd预测地图",
  253. description="直接从数据库读取数据生成有效态Cd预测地图")
  254. async def generate_effective_cd_map_from_database(
  255. area: str = Form(..., description="区域名称,如:乐昌市"),
  256. level: str = Form("auto", description="行政级别,如:county, city, province,默认为auto自动识别"),
  257. enable_interpolation: Optional[bool] = Form(None, description="是否启用空间插值"),
  258. interpolation_method: Optional[str] = Form(None, description="插值方法: nearest, linear, cubic"),
  259. resolution_factor: Optional[float] = Form(None, description="分辨率因子,越大分辨率越高")
  260. ):
  261. """
  262. 基于数据库数据生成有效态Cd预测地图
  263. @param area: 区域名称
  264. @param level: 行政级别
  265. @returns {FileResponse} 预测地图文件
  266. """
  267. try:
  268. logger.info(f"开始基于数据库数据为{area}({level})生成有效态Cd预测地图")
  269. # 构建栅格配置参数
  270. raster_params = {}
  271. if enable_interpolation is not None:
  272. raster_params['enable_interpolation'] = enable_interpolation
  273. if interpolation_method is not None:
  274. raster_params['interpolation_method'] = interpolation_method
  275. if resolution_factor is not None:
  276. raster_params['resolution_factor'] = resolution_factor
  277. # 使用数据库数据生成预测
  278. db_service = CdPredictionDatabaseService()
  279. result = await db_service.generate_effective_cd_prediction_from_database(
  280. area=area,
  281. level=level,
  282. raster_config_override=raster_params if raster_params else None
  283. )
  284. if not result['map_path'] or not os.path.exists(result['map_path']):
  285. raise HTTPException(status_code=500, detail="基于数据库数据的地图文件生成失败")
  286. logger.info(f"基于数据库数据为{area}({level})生成有效态Cd预测地图成功,处理{result.get('processed_records', 0)}条记录")
  287. return FileResponse(
  288. path=result['map_path'],
  289. filename=f"{area}_effective_cd_prediction_map_database.jpg",
  290. media_type="image/jpeg"
  291. )
  292. except HTTPException:
  293. raise
  294. except Exception as e:
  295. logger.error(f"基于数据库数据为{area}({level})生成有效态Cd预测地图失败: {str(e)}")
  296. raise HTTPException(
  297. status_code=500,
  298. detail=f"基于数据库数据为{area}({level})生成有效态Cd预测地图失败: {str(e)}"
  299. )
  300. # =============================================================================
  301. # 获取最新预测结果接口(无需重新计算)
  302. # =============================================================================
  303. @router.get("/crop-cd/latest-map/{area_name}",
  304. summary="获取作物Cd最新地图",
  305. description="直接返回指定区域的最新作物Cd预测地图,无需重新计算")
  306. async def get_latest_crop_cd_map(area_name: str):
  307. """
  308. 获取指定区域的最新作物Cd预测地图
  309. @param area_name: 区域名称,如:乐昌市
  310. @returns {FileResponse} 最新的预测地图文件
  311. """
  312. try:
  313. logger.info(f"获取{area_name}的最新作物Cd预测地图")
  314. service = CdPredictionServiceV3()
  315. # 查找最新的地图文件
  316. map_pattern = f"prediction_map_crop_cd_{area_name}_*.jpg"
  317. map_files = []
  318. # 在输出目录中查找相关文件
  319. import glob
  320. output_dir = service.output_figures_dir
  321. search_pattern = os.path.join(output_dir, map_pattern)
  322. map_files = glob.glob(search_pattern)
  323. if not map_files:
  324. raise HTTPException(
  325. status_code=404,
  326. detail=f"未找到{area_name}的作物Cd预测地图,请先执行预测"
  327. )
  328. # 选择最新的文件(按修改时间排序)
  329. latest_map = max(map_files, key=os.path.getmtime)
  330. if not os.path.exists(latest_map):
  331. raise HTTPException(status_code=404, detail="地图文件不存在")
  332. return FileResponse(
  333. path=latest_map,
  334. filename=f"{area_name}_latest_crop_cd_map.jpg",
  335. media_type="image/jpeg"
  336. )
  337. except HTTPException:
  338. raise
  339. except Exception as e:
  340. logger.error(f"获取{area_name}最新作物Cd地图失败: {str(e)}")
  341. raise HTTPException(
  342. status_code=500,
  343. detail=f"获取{area_name}最新作物Cd地图失败: {str(e)}"
  344. )
  345. @router.get("/effective-cd/latest-map/{area_name}",
  346. summary="获取有效态Cd最新地图",
  347. description="直接返回指定区域的最新有效态Cd预测地图,无需重新计算")
  348. async def get_latest_effective_cd_map(area_name: str):
  349. """
  350. 获取指定区域的最新有效态Cd预测地图
  351. @param area_name: 区域名称,如:乐昌市
  352. @returns {FileResponse} 最新的预测地图文件
  353. """
  354. try:
  355. logger.info(f"获取{area_name}的最新有效态Cd预测地图")
  356. service = CdPredictionServiceV3()
  357. # 查找最新的地图文件
  358. map_pattern = f"prediction_map_effective_cd_{area_name}_*.jpg"
  359. map_files = []
  360. # 在输出目录中查找相关文件
  361. import glob
  362. output_dir = service.output_figures_dir
  363. search_pattern = os.path.join(output_dir, map_pattern)
  364. map_files = glob.glob(search_pattern)
  365. if not map_files:
  366. raise HTTPException(
  367. status_code=404,
  368. detail=f"未找到{area_name}的有效态Cd预测地图,请先执行预测"
  369. )
  370. # 选择最新的文件(按修改时间排序)
  371. latest_map = max(map_files, key=os.path.getmtime)
  372. if not os.path.exists(latest_map):
  373. raise HTTPException(status_code=404, detail="地图文件不存在")
  374. return FileResponse(
  375. path=latest_map,
  376. filename=f"{area_name}_latest_effective_cd_map.jpg",
  377. media_type="image/jpeg"
  378. )
  379. except HTTPException:
  380. raise
  381. except Exception as e:
  382. logger.error(f"获取{area_name}最新有效态Cd地图失败: {str(e)}")
  383. raise HTTPException(
  384. status_code=500,
  385. detail=f"获取{area_name}最新有效态Cd地图失败: {str(e)}"
  386. )
  387. @router.get("/crop-cd/latest-histogram/{area_name}",
  388. summary="获取作物Cd最新直方图",
  389. description="直接返回指定区域的最新作物Cd预测直方图,无需重新计算")
  390. async def get_latest_crop_cd_histogram(area_name: str):
  391. """
  392. 获取指定区域的最新作物Cd预测直方图
  393. @param area_name: 区域名称,如:乐昌市
  394. @returns {FileResponse} 最新的预测直方图文件
  395. """
  396. try:
  397. logger.info(f"获取{area_name}的最新作物Cd预测直方图")
  398. service = CdPredictionServiceV3()
  399. # 查找最新的直方图文件
  400. histogram_pattern = f"prediction_histogram_crop_cd_{area_name}_*.jpg"
  401. histogram_files = []
  402. # 在输出目录中查找相关文件
  403. import glob
  404. output_dir = service.output_figures_dir
  405. search_pattern = os.path.join(output_dir, histogram_pattern)
  406. histogram_files = glob.glob(search_pattern)
  407. if not histogram_files:
  408. raise HTTPException(
  409. status_code=404,
  410. detail=f"未找到{area_name}的作物Cd预测直方图,请先执行预测"
  411. )
  412. # 选择最新的文件(按修改时间排序)
  413. latest_histogram = max(histogram_files, key=os.path.getmtime)
  414. if not os.path.exists(latest_histogram):
  415. raise HTTPException(status_code=404, detail="直方图文件不存在")
  416. return FileResponse(
  417. path=latest_histogram,
  418. filename=f"{area_name}_latest_crop_cd_histogram.jpg",
  419. media_type="image/jpeg"
  420. )
  421. except HTTPException:
  422. raise
  423. except Exception as e:
  424. logger.error(f"获取{area_name}最新作物Cd直方图失败: {str(e)}")
  425. raise HTTPException(
  426. status_code=500,
  427. detail=f"获取{area_name}最新作物Cd直方图失败: {str(e)}"
  428. )
  429. @router.get("/effective-cd/latest-histogram/{area_name}",
  430. summary="获取有效态Cd最新直方图",
  431. description="直接返回指定区域的最新有效态Cd预测直方图,无需重新计算")
  432. async def get_latest_effective_cd_histogram(area_name: str):
  433. """
  434. 获取指定区域的最新有效态Cd预测直方图
  435. @param area_name: 区域名称,如:乐昌市
  436. @returns {FileResponse} 最新的预测直方图文件
  437. """
  438. try:
  439. logger.info(f"获取{area_name}的最新有效态Cd预测直方图")
  440. service = CdPredictionServiceV3()
  441. # 查找最新的直方图文件
  442. histogram_pattern = f"prediction_histogram_effective_cd_{area_name}_*.jpg"
  443. histogram_files = []
  444. # 在输出目录中查找相关文件
  445. import glob
  446. output_dir = service.output_figures_dir
  447. search_pattern = os.path.join(output_dir, histogram_pattern)
  448. histogram_files = glob.glob(search_pattern)
  449. if not histogram_files:
  450. raise HTTPException(
  451. status_code=404,
  452. detail=f"未找到{area_name}的有效态Cd预测直方图,请先执行预测"
  453. )
  454. # 选择最新的文件(按修改时间排序)
  455. latest_histogram = max(histogram_files, key=os.path.getmtime)
  456. if not os.path.exists(latest_histogram):
  457. raise HTTPException(status_code=404, detail="直方图文件不存在")
  458. return FileResponse(
  459. path=latest_histogram,
  460. filename=f"{area_name}_latest_effective_cd_histogram.jpg",
  461. media_type="image/jpeg"
  462. )
  463. except HTTPException:
  464. raise
  465. except Exception as e:
  466. logger.error(f"获取{area_name}最新有效态Cd直方图失败: {str(e)}")
  467. raise HTTPException(
  468. status_code=500,
  469. detail=f"获取{area_name}最新有效态Cd直方图失败: {str(e)}"
  470. )