video_location.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. # -*- coding: utf-8 -*-
  2. """
  3. 最终交付版 - Python后端GeoJSON处理器 (v11.2)
  4. 功能:
  5. - 完全基于您提供的、投影方向正确的 project_lonlat_to_pixel 函数。
  6. - 核心函数 process_geojson_for_frontend 直接输出一个“干净”的、可供前端JS绘制的【线段列表】。
  7. - 包含一个本地OpenCV预览,用于即时验证返回给前端的数据。
  8. """
  9. import numpy as np
  10. from scipy.spatial.transform import Rotation as R
  11. import json
  12. ## -------------------------------------------------------------------------- ##
  13. ## 1. 核心算法函数 (基于您的版本)
  14. ## -------------------------------------------------------------------------- ##
  15. EARTH_RADIUS = 6378137.0
  16. def lonlat_to_enu(lon, lat, center_lon, center_lat):
  17. delta_lon = np.radians(lon - center_lon)
  18. delta_lat = np.radians(lat - center_lat)
  19. x = EARTH_RADIUS * delta_lon * np.cos(np.radians(center_lat))
  20. y = EARTH_RADIUS * delta_lat
  21. return np.array([x, y, 0])
  22. def compute_hfov_by_zoom(hfov_max, zoom, camera_type="dahua"):
  23. """
  24. 将缩放等级(zoom)映射为水平视场角(hfov)。
  25. 规则:
  26. - 最小zoom对应最大视场角(hfov_max)
  27. - 最大zoom对应最小视场角(hfov_max * min_zoom / max_zoom)
  28. """
  29. if zoom is None:
  30. return hfov_max
  31. camera_type = (camera_type or "dahua").lower()
  32. if camera_type in ("hik", "hikvision", "hk", "haikang", "海康"):
  33. min_zoom, max_zoom = 1.0, 56.0
  34. else:
  35. min_zoom, max_zoom = 0.4375, 56.0
  36. # clamp zoom to valid range
  37. z = float(zoom)
  38. if z < min_zoom:
  39. z = min_zoom
  40. elif z > max_zoom:
  41. z = max_zoom
  42. # assume hfov scales inversely with zoom
  43. return hfov_max * (min_zoom / z)
  44. def project_lonlat_to_pixel(target_lon, target_lat, resolution_w, resolution_h,
  45. cam_lon, cam_lat, cam_height, p, t, hfov, north_p_value=0.0):
  46. """
  47. [最终正确版] 反向投影函数:从经纬度计算像素坐标(u, v)。
  48. - 正确处理 cz < 0 的情况,通过创建一个方向正确但位置在无穷远的虚拟点来实现线段裁剪。
  49. """
  50. # 步骤 1 & 2: ... (这部分代码不变)
  51. target_enu = lonlat_to_enu(target_lon, target_lat, cam_lon, cam_lat)
  52. cam_pos_enu = np.array([0, 0, cam_height])
  53. vec_enu = target_enu - cam_pos_enu
  54. norm = np.linalg.norm(vec_enu)
  55. if norm < 1e-6:
  56. return None, None, False
  57. vec_enu /= norm
  58. # 步骤 3: ... (这部分代码不变)
  59. p_corrected = p - north_p_value
  60. r_base = R.from_euler('x', -90, degrees=True)
  61. r_pan = R.from_euler('z', p_corrected, degrees=True)
  62. r_tilt = R.from_euler('x', -t, degrees=True)
  63. total_rotation = r_pan * r_tilt * r_base
  64. inverse_rotation = total_rotation.inv()
  65. vec_cam = inverse_rotation.apply(vec_enu)
  66. cx, cy, cz = vec_cam
  67. center_u = resolution_w / 2
  68. center_v = resolution_h / 2
  69. # ===================== [ 核心修正逻辑 V3.0 ] =====================
  70. # 如果点在相机后方 (cz <= 0)
  71. if cz <= 1e-9:
  72. # 我们不再尝试投影它。
  73. # 而是根据 cx, cy 的方向,在屏幕中心的基础上,向该方向延伸一个巨大的距离,
  74. # 创建一个“无穷远”的虚拟点。这个点的方向是正确的。
  75. # 这个巨大的数值确保了该点一定在屏幕外。
  76. MEGA_DISTANCE = 1_000_000.0
  77. u = center_u + cx * MEGA_DISTANCE
  78. v = center_v + cy * MEGA_DISTANCE
  79. return u, v, False # 这个点本身是不可见的
  80. # =================================================================
  81. # 步骤 4: (正常投影逻辑) 如果点在相机前方,则正常计算
  82. f_x = resolution_w / (2 * np.tan(np.radians(hfov) / 2))
  83. f_y = f_x
  84. u = center_u + f_x * (cx / cz)
  85. v = center_v + f_y * (cy / cz)
  86. is_visible = (0 <= u < resolution_w) and (0 <= v < resolution_h)
  87. return u, v, is_visible
  88. ## -------------------------------------------------------------------------- ##
  89. ## 2. GeoJSON解析 与 线段裁剪算法
  90. ## -------------------------------------------------------------------------- ##
  91. def parse_geojson_coordinates(geojson_data):
  92. coordinates = []
  93. try:
  94. if geojson_data.get('type') == 'FeatureCollection': geometry = geojson_data['features'][0]['geometry']
  95. else: geometry = geojson_data
  96. if geometry.get('type') == 'Polygon':
  97. exterior_ring = geometry['coordinates'][0]
  98. coordinates = [(coord[0], coord[1]) for coord in exterior_ring]
  99. print(f"成功从GeoJSON中提取了 {len(coordinates)} 个边界点。")
  100. return coordinates
  101. except (KeyError, IndexError, TypeError): pass
  102. return []
  103. INSIDE, LEFT, RIGHT, BOTTOM, TOP = 0, 1, 2, 4, 8
  104. def compute_outcode(x, y, xmin, ymin, xmax, ymax):
  105. code = INSIDE
  106. if x < xmin: code |= LEFT
  107. elif x > xmax: code |= RIGHT
  108. if y < ymin: code |= BOTTOM
  109. elif y > ymax: code |= TOP
  110. return code
  111. def cohen_sutherland_clip(x1, y1, x2, y2, xmin, ymin, xmax, ymax):
  112. code1 = compute_outcode(x1, y1, xmin, ymin, xmax, ymax); code2 = compute_outcode(x2, y2, xmin, ymin, xmax, ymax)
  113. accept = False
  114. while True:
  115. if not (code1 | code2): accept = True; break
  116. elif code1 & code2: break
  117. else:
  118. x, y = 0.0, 0.0; code_out = code1 if code1 else code2
  119. if code_out & TOP:
  120. if y2 != y1: x = x1 + (x2 - x1) * (ymax - y1) / (y2 - y1)
  121. else: x = x1
  122. y = ymax
  123. elif code_out & BOTTOM:
  124. if y2 != y1: x = x1 + (x2 - x1) * (ymin - y1) / (y2 - y1)
  125. else: x = x1
  126. y = ymin
  127. elif code_out & RIGHT:
  128. if x2 != x1: y = y1 + (y2 - y1) * (xmax - x1) / (x2 - x1)
  129. else: y = y1
  130. x = xmax
  131. elif code_out & LEFT:
  132. if x2 != x1: y = y1 + (y2 - y1) * (xmin - x1) / (x2 - x1)
  133. else: y = y1
  134. x = xmin
  135. if code_out == code1: x1, y1 = x, y; code1 = compute_outcode(x1, y1, xmin, ymin, xmax, ymax)
  136. else: x2, y2 = x, y; code2 = compute_outcode(x2, y2, xmin, ymin, xmax, ymax)
  137. if accept: return [[int(x1), int(y1)], [int(x2), int(y2)]]
  138. return None
  139. ## -------------------------------------------------------------------------- ##
  140. ## 3. 核心处理函数
  141. ## -------------------------------------------------------------------------- ##
  142. def process_geojson_for_frontend(geojson_data, camera_params, calibrated_params):
  143. geo_coords = parse_geojson_coordinates(geojson_data)
  144. if not geo_coords: return []
  145. if len(geo_coords) > 1 and geo_coords[0] != geo_coords[-1]:
  146. geo_coords.append(geo_coords[0]) # 确保多边形闭合
  147. raw_pixel_points = []
  148. hfov_max = float(calibrated_params['hfov'])
  149. zoom_value = camera_params.get('z')
  150. if zoom_value is None:
  151. zoom_value = camera_params.get('zoom')
  152. hfov = compute_hfov_by_zoom(
  153. hfov_max=hfov_max,
  154. zoom=zoom_value,
  155. camera_type=camera_params.get('camera_type', 'dahua')
  156. )
  157. # 1. Batch convert all geographic points to pixel points
  158. for lon, lat in geo_coords:
  159. u, v, is_visible = project_lonlat_to_pixel(
  160. lon, lat,
  161. resolution_w=int(camera_params['resolution_w']), resolution_h=int(camera_params['resolution_h']),
  162. cam_lon=float(camera_params['cam_lon']), cam_lat=float(camera_params['cam_lat']),
  163. cam_height=float(calibrated_params['cam_height']), p=float(camera_params['p']),
  164. t=float(camera_params['t']), hfov=hfov,
  165. north_p_value=float(camera_params['north_p_value'])
  166. )
  167. raw_pixel_points.append([int(u), int(v)])
  168. visible_segments = []
  169. screen_w = int(camera_params['resolution_w'])
  170. screen_h = int(camera_params['resolution_h'])
  171. for i in range(len(raw_pixel_points) - 1):
  172. p1 = raw_pixel_points[i]
  173. p2 = raw_pixel_points[i+1]
  174. if p1[0] == float('inf') or p2[0] == float('inf'): continue
  175. u1, v1 = p1; u2, v2 = p2
  176. clipped_segment = cohen_sutherland_clip(u1, v1, u2, v2, 0, 0, screen_w - 1, screen_h - 1)
  177. if clipped_segment:
  178. visible_segments.append(clipped_segment)
  179. return visible_segments
  180. ## -------------------------------------------------------------------------- ##
  181. ## 4. 主程序
  182. ## -------------------------------------------------------------------------- ##
  183. if __name__ == '__main__':
  184. geojson_string = """
  185. {
  186. "type": "FeatureCollection",
  187. "features": [
  188. {
  189. "type": "Feature",
  190. "properties": {
  191. "name": "",
  192. "id": "m-7d43a6d0-a7bd-4fc1-84b2-deab51757f8e",
  193. "type": "polygon",
  194. "style": {
  195. "color": "#29cf34",
  196. "opacity": 0.5,
  197. "outline": true,
  198. "outlineWidth": 3,
  199. "outlineColor": "#ffffff",
  200. "clampToGround": true,
  201. "materialType": "Color",
  202. "label": {
  203. "text": "",
  204. "font_size": 18,
  205. "color": "#ffffff",
  206. "outline": true,
  207. "outlineColor": "#000000",
  208. "pixelOffsetY": -10,
  209. "distanceDisplayCondition": true,
  210. "distanceDisplayCondition_far": 500000,
  211. "distanceDisplayCondition_near": 0
  212. }
  213. },
  214. "options": {}
  215. },
  216. "geometry": {
  217. "type": "Polygon",
  218. "coordinates": [
  219. [
  220. [
  221. 112.894539,
  222. 28.221327,
  223. 0
  224. ],
  225. [
  226. 112.894712,
  227. 28.221333,
  228. 0
  229. ],
  230. [
  231. 112.894709,
  232. 28.221267,
  233. 0
  234. ],
  235. [
  236. 112.894533,
  237. 28.221267,
  238. 0
  239. ]
  240. ]
  241. ]
  242. }
  243. }
  244. ]
  245. }
  246. """
  247. geojson_data = json.loads(geojson_string)
  248. calibrated_params = {"cam_height": 28.0000, "hfov": 61.0000}
  249. camera_params = {
  250. "resolution_w": 1280, "resolution_h": 720,
  251. "cam_lon": 112.893799, "cam_lat": 28.221701,
  252. "north_p_value": 39.0, "p": 271.9, "t": 26.2,
  253. "camera_type": "dahua",
  254. "z": 0.4375
  255. }
  256. final_screen_segments = process_geojson_for_frontend(geojson_data, camera_params, calibrated_params)
  257. print(json.dumps(final_screen_segments, indent=2))