video_location.py 10 KB

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