浏览代码

初始化

wzh 4 月之前
当前提交
da6024dbca
共有 7 个文件被更改,包括 661 次插入0 次删除
  1. 95 0
      app.py
  2. 162 0
      calibrate.py
  3. 8 0
      config.ini
  4. 0 0
      config/__init__.py
  5. 28 0
      config/config_util.py
  6. 121 0
      pixel_to_lonlat.py
  7. 247 0
      video_location.py

+ 95 - 0
app.py

@@ -0,0 +1,95 @@
+import threading
+import time
+
+from flask import Flask, jsonify, request
+import json
+
+app = Flask(__name__)
+
+from config.config_util import ConfigUtil
+
+from calibrate import run_ransac_calibration
+from video_location import process_geojson_for_frontend
+
+from pixel_to_lonlat import pixel_to_lonlat
+import logging
+# 配置全局日志
+logging.basicConfig(
+    filename='app.log',  # 日志文件
+    level=logging.INFO,  # 记录 DEBUG 及以上级别日志
+    format='%(asctime)s [%(levelname)s] %(name)s: %(message)s'
+)
+# logging.basicConfig(
+#     level=logging.INFO,
+#     format='%(asctime)s [%(levelname)s] %(name)s: %(message)s',
+#     handlers=[
+#         logging.FileHandler('app.log'),  # 日志文件
+#         logging.StreamHandler()  # 控制台输出
+#     ]
+# )
+
+print("app init")
+
+
+logger = logging.getLogger("application")
+
+
+
+
+
+
+
+
+
+#校准
+@app.route('/calibrate',methods=['POST'])
+def calibrate():
+    data = request.get_json()
+    all_calibration_data = data.get("points")
+    list_of_tuples = [
+        (point['u'], point['v'], point['lon'], point['lat'])
+        for point in all_calibration_data
+    ]
+    camera_params = data.get("camera_params")
+    initial_guesses = data.get("initial_guesses")
+    final_height, final_hfov, inliers = run_ransac_calibration(
+        all_points=list_of_tuples, camera_params=camera_params, initial_guesses=initial_guesses,
+        ransac_iterations=100, subset_size=3, error_threshold=3.0)
+    code = 0
+    if final_height is not None:
+        code = 1
+    return jsonify({"final_height": str(final_height), "final_hfov": str(final_hfov),"code":code})
+
+##将geojson转成坐标
+@app.route('/videoLocation',methods=['POST'])
+def videoLocation():
+    data = request.get_json()
+    geojson = data.get("geojson")
+    camera_params = data.get("camera_params")
+    calibrated_params = data.get("calibrated_params")
+    final_screen_segments = process_geojson_for_frontend(geojson, camera_params, calibrated_params)
+    code = 0
+    return jsonify({"code" : code,"list" : final_screen_segments})
+
+#将点集合转成经纬度
+@app.route('/parseLocation',methods=['POST'])
+def parseLocation():
+    data = request.get_json()
+    points = []
+    all_calibration_data = data.get("points")
+    camera_params = data.get("camera_params")
+    current_ptz_params = data.get("current_ptz_params")
+    for point in all_calibration_data:
+        res = pixel_to_lonlat(point['u'], point['v'], camera_params, current_ptz_params)
+        points.append(res)
+    code = 0
+    return jsonify({"code" : code,"points" : points})
+
+
+
+
+
+if __name__ == '__main__':
+    # 从 Flask 配置中获取端口
+    port = int(ConfigUtil.get('server', 'port'))
+    app.run(host='0.0.0.0', port=port)

+ 162 - 0
calibrate.py

@@ -0,0 +1,162 @@
+# -*- coding: utf-8 -*-
+"""
+带有RANSAC功能的摄像头自动标定工具 (v3.2 结构优化版)
+
+功能:
+- RANSAC算法自动剔除“坏点”。
+- 新增 calculate_final_parameters 函数,使用所有“好点”进行最终的联合优化,计算出最精确结果。
+
+v3.2 更新日志:
+- 将最终优化步骤封装到独立的 calculate_final_parameters 函数中,使流程更清晰。
+"""
+import numpy as np
+from scipy.optimize import minimize
+from scipy.spatial.transform import Rotation as R
+import random
+import warnings
+
+warnings.filterwarnings("ignore", category=RuntimeWarning)
+
+## -------------------------------------------------------------------------- ##
+## 1. 核心算法函数 (无变化)
+## -------------------------------------------------------------------------- ##
+EARTH_RADIUS = 6378137.0
+def lonlat_to_enu(lon, lat, center_lon, center_lat):
+    delta_lon = np.radians(lon - center_lon); delta_lat = np.radians(lat - center_lat)
+    x = EARTH_RADIUS * delta_lon * np.cos(np.radians(center_lat)); y = EARTH_RADIUS * delta_lat
+    return np.array([x, y, 0])
+def enu_to_lonlat(enu_point, center_lon, center_lat):
+    x, y, _ = enu_point; delta_lat_rad = y / EARTH_RADIUS
+    delta_lon_rad = x / (EARTH_RADIUS * np.cos(np.radians(center_lat)))
+    lon = center_lon + np.degrees(delta_lon_rad); lat = center_lat + np.degrees(delta_lat_rad)
+    return lon, lat
+def project_pixel_to_lonlat(u, v, resolution_w, resolution_h, cam_lon, cam_lat,
+                            cam_height, p, t, hfov, north_p_value=0.0, **kwargs):
+    f_x = resolution_w / (2 * np.tan(np.radians(hfov) / 2)); f_y = f_x
+    cx = resolution_w / 2; cy = resolution_h / 2
+    vec_cam = np.array([(u - cx) / f_x, (v - cy) / f_y, 1.0]); vec_cam /= np.linalg.norm(vec_cam)
+    r_base = R.from_euler('x', -90, degrees=True); p_corrected = p - north_p_value
+    r_pan = R.from_euler('z', p_corrected, degrees=True); r_tilt = R.from_euler('x', -t, degrees=True)
+    total_rotation = r_pan * r_tilt * r_base; vec_enu = total_rotation.apply(vec_cam)
+    if vec_enu[2] >= -1e-9: return None, None
+    cam_pos_enu = np.array([0, 0, cam_height]); s = -cam_height / vec_enu[2]
+    intersection_enu = cam_pos_enu + s * vec_enu
+    return enu_to_lonlat(intersection_enu, cam_lon, cam_lat)
+
+## -------------------------------------------------------------------------- ##
+## 2. RANSAC 及最终计算逻辑
+## -------------------------------------------------------------------------- ##
+
+def solve_model_for_subset(subset_points, camera_params, initial_guesses):
+    """(RANSAC内部使用) 使用一个小的点集来快速计算一个模型参数的估计值"""
+    static_args = camera_params.copy()
+    initial_values = [initial_guesses['height'], initial_guesses['hfov']]
+    def objective(params):
+        total_error = 0.0
+        for point in subset_points:
+            pred_lon, pred_lat = project_pixel_to_lonlat(point[0], point[1], cam_height=params[0], hfov=params[1], **static_args)
+            if pred_lon is None: return 1e12
+            true_enu = lonlat_to_enu(point[2], point[3], camera_params['cam_lon'], camera_params['cam_lat']); pred_enu = lonlat_to_enu(pred_lon, pred_lat, camera_params['cam_lon'], camera_params['cam_lat'])
+            total_error += np.sum((pred_enu - true_enu)**2)
+        return total_error
+    result = minimize(objective, initial_values, method='Nelder-Mead', options={'xatol': 1e-2, 'fatol': 1e-2})
+    if result.success: return result.x[0], result.x[1]
+    return None, None
+
+def calculate_final_parameters(inliers, camera_params, initial_guesses):
+    """
+    [新增方法] 使用所有“好点”(inliers)进行最终的联合优化,计算最精确的平均高度和HFOV。
+    """
+    print("\n--- 使用所有内点进行最终优化 ---")
+    static_args = camera_params.copy()
+    initial_values = [initial_guesses['height'], initial_guesses['hfov']]
+
+    def objective(params):
+        total_error = 0.0
+        for point in inliers:
+            pred_lon, pred_lat = project_pixel_to_lonlat(point[0], point[1], cam_height=params[0], hfov=params[1], **static_args)
+            if pred_lon is None: return 1e12
+            true_enu = lonlat_to_enu(point[2], point[3], camera_params['cam_lon'], camera_params['cam_lat']); pred_enu = lonlat_to_enu(pred_lon, pred_lat, camera_params['cam_lon'], camera_params['cam_lat'])
+            error = np.sum((pred_enu - true_enu)**2); total_error += error
+        return total_error
+
+    result = minimize(objective, initial_values, method='Nelder-Mead', options={'disp': True, 'xatol': 1e-6, 'fatol': 1e-6})
+
+    if result.success and result.fun < 1e11:
+        return result.x[0], result.x[1], result.fun
+    else:
+        return (result.x[0], result.x[1], result.fun)
+
+def run_ransac_calibration(all_points, camera_params, initial_guesses,
+                           ransac_iterations=100, subset_size=3, error_threshold=3.0):
+    """执行RANSAC算法来寻找最佳的内点集。"""
+    print("--- 开始执行RANSAC自动标定 ---")
+    print(f"总点数: {len(all_points)}, RANSAC迭代次数: {ransac_iterations}, 误差阈值: {error_threshold}米")
+
+    best_inliers, best_model_params = [], (0, 0)
+
+    for i in range(ransac_iterations):
+        if len(all_points) < subset_size: print("错误:总点数小于子集大小。"); return None, None, []
+
+        subset = random.sample(all_points, subset_size)
+        height_hyp, hfov_hyp = solve_model_for_subset(subset, camera_params, initial_guesses)
+        if height_hyp is None: continue
+
+        current_inliers = []
+        for point in all_points:
+            pred_lon, pred_lat = project_pixel_to_lonlat(point[0], point[1], cam_height=height_hyp, hfov=hfov_hyp, **camera_params)
+            if pred_lon is not None:
+                true_enu = lonlat_to_enu(point[2], point[3], camera_params['cam_lon'], camera_params['cam_lat']); pred_enu = lonlat_to_enu(pred_lon, pred_lat, camera_params['cam_lon'], camera_params['cam_lat'])
+                error = np.linalg.norm(true_enu - pred_enu)
+                if error < error_threshold: current_inliers.append(point)
+
+        if len(current_inliers) > len(best_inliers):
+            best_inliers = current_inliers
+            best_model_params = (height_hyp, hfov_hyp)
+            print(f"  迭代 {i+1}/{ransac_iterations}: 发现更好的模型,内点数量 = {len(best_inliers)}")
+
+    if not best_inliers:
+        print("\nRANSAC失败:未找到一致的点集。"); return None, None, []
+
+    print(f"\nRANSAC完成:找到的最佳模型有 {len(best_inliers)} 个内点。")
+
+    # [结构调整] 调用新增的函数进行最终计算
+    final_height, final_hfov, _ = calculate_final_parameters(best_inliers, camera_params,
+                                                             {"height": best_model_params[0], "hfov": best_model_params[1]})
+
+    return final_height, final_hfov, best_inliers
+
+## -------------------------------------------------------------------------- ##
+## 3. 数据输入与执行
+## -------------------------------------------------------------------------- ##
+if __name__ == '__main__':
+    all_calibration_data = [
+        (548, 196, 112.894553, 28.221280), (91, 276, 112.894612, 28.221594), (216, 189, 112.894757, 28.221469),
+        (850, 250, 112.89487, 28.22086), (450, 450, 112.89389, 28.22137), (1100, 500, 112.89468, 28.22050),
+        (610, 415, 112.8955, 28.2220), (362, 527, 112.8930, 28.2210),
+    ]
+    camera_params = {
+        "resolution_w": 1280, "resolution_h": 720, "cam_lon": 112.893799, "cam_lat": 28.221701,
+        "north_p_value": 39.0, "p": 271.9, "t": 26.2
+    }
+    initial_guesses = {"height": 0, "hfov": 0}
+
+    final_height, final_hfov, inliers = run_ransac_calibration(
+        all_points=all_calibration_data, camera_params=camera_params, initial_guesses=initial_guesses,
+        ransac_iterations=100, subset_size=3, error_threshold=3.0)
+
+    if final_height is not None:
+        print("\n" + "#"*50); print("#" + " "*12 + "RANSAC 自动标定成功完成" + " "*13 + "#"); print("#"*50)
+        print(f"# >>> 最终标定结果 (Final Calibrated Results):")
+        print(f"#   - 摄像头高度 (Height): {final_height:.4f} 米")
+        print(f"#   - 水平视场角 (HFOV): {final_hfov:.4f} 度")
+        print("#" + "-"*48 + "#")
+        print(f"# 共找到 {len(inliers)} 个一致的“好点”(Inliers):")
+        inlier_pixels = {(p[0], p[1]) for p in inliers}
+        for point in all_calibration_data:
+            if (point[0], point[1]) in inlier_pixels: print(f"#   - 像素点 ({point[0]}, {point[1]},{point[2]}, {point[3]}) [内点]")
+        outliers = [p for p in all_calibration_data if (p[0], p[1]) not in inlier_pixels]
+        if outliers:
+            print(f"# 共剔除 {len(outliers)} 个“坏点”(Outliers):")
+            for point in outliers: print(f"#   - 像素点 ({point[0]}, {point[1]},{point[2]}, {point[3]}) [外点]")
+        print("#"*50)

+ 8 - 0
config.ini

@@ -0,0 +1,8 @@
+[server]
+port = 5000
+
+
+
+
+
+

+ 0 - 0
config/__init__.py


+ 28 - 0
config/config_util.py

@@ -0,0 +1,28 @@
+import configparser
+import os
+
+class ConfigUtil:
+    _config = None
+    _loaded = False
+
+    @classmethod
+    def _init(cls):
+        if cls._loaded:
+            return
+        config_path = os.path.join(os.path.dirname(__file__), '../config.ini')
+        parser = configparser.ConfigParser()
+        parser.read(config_path, encoding='utf-8')
+        cls._config = parser
+        cls._loaded = True
+
+    @classmethod
+    def get(cls, section, option, fallback=None):
+        cls._init()
+        return cls._config.get(section, option, fallback=fallback)
+
+    @classmethod
+    def get_list(cls, section, option, sep=',', fallback=None):
+        value = cls.get(section, option, fallback)
+        if value is not None:
+            return [v.strip() for v in value.split(sep)]
+        return fallback

+ 121 - 0
pixel_to_lonlat.py

@@ -0,0 +1,121 @@
+# -*- coding: utf-8 -*-
+"""
+最终功能模块:从像素坐标到经纬度的转换
+
+核心功能:
+- pixel_to_lonlat: 输入像素坐标(u, v)和摄像头状态,输出对应的经纬度。
+"""
+import numpy as np
+from scipy.spatial.transform import Rotation as R
+
+# --- 核心函数 ---
+
+def pixel_to_lonlat(u, v, camera_params, ptz_params):
+    """
+    正向投影:将像素坐标(u, v)转换为经纬度。
+
+    Args:
+        u (int): 像素横坐标
+        v (int): 像素纵坐标
+        camera_params (dict): 摄像头的固定物理参数 (通过标定获得)
+        ptz_params (dict): 摄像头当前的云台参数
+
+    Returns:
+        tuple: (经度, 纬度) 或 (None, None) 如果计算失败
+    """
+    # --- 1. 解包所有已知参数 ---
+    res_w, res_h = camera_params['resolution_w'], camera_params['resolution_h']
+    cam_lon, cam_lat = camera_params['cam_lon'], camera_params['cam_lat']
+    cam_height, hfov = camera_params['height'], camera_params['hfov']
+    north_p = camera_params['north_p_value']
+    p, t = ptz_params['pan'], ptz_params['tilt']
+
+    # --- 2. 相机内参模型:将像素(u,v)转换为相机坐标系下的方向向量 ---
+    # (相机坐标系定义: x向右, y向下, z向前)
+    f_x = res_w / (2 * np.tan(np.radians(hfov) / 2))
+    f_y = f_x  # 假设像素是正方形
+    cx, cy = res_w / 2, res_h / 2
+
+    # 计算方向向量并归一化
+    vec_cam = np.array([(u - cx) / f_x, (v - cy) / f_y, 1.0])
+    vec_cam /= np.linalg.norm(vec_cam)
+
+    # --- 3. 旋转变换:将相机坐标系下的向量旋转到世界坐标系 ---
+    # (世界坐标系定义: ENU, 即 x向东, y向北, z向上)
+    r_base = R.from_euler('x', -90, degrees=True) # 基础旋转:将(相机前)对准(世界北)
+    p_corrected = p - north_p                     # 校正P值,使其以正北为0度
+    r_pan = R.from_euler('z', p_corrected, degrees=True)  # 方位角旋转 (绕世界Z轴)
+    r_tilt = R.from_euler('x', -t, degrees=True)          # 俯仰角旋转 (绕世界X轴)
+
+    # 合成总旋转
+    total_rotation = r_pan * r_tilt * r_base
+    vec_enu = total_rotation.apply(vec_cam)
+
+    # --- 4. 几何解算:计算光线与地面的交点 ---
+    # 如果光线向量的z分量是正数或零,说明光线朝向天空或水平,无法与地面相交
+    if vec_enu[2] >= -1e-9:
+        return None, None
+
+    # 摄像头在世界坐标系中的位置
+    cam_pos_enu = np.array([0, 0, cam_height])
+
+    # 求解射线参数 s
+    s = -cam_height / vec_enu[2]
+
+    # 计算交点坐标 (米)
+    intersection_enu = cam_pos_enu + s * vec_enu
+
+    # --- 5. 坐标转换:将米制坐标转回经纬度 ---
+    EARTH_RADIUS = 6378137.0
+    delta_lat_rad = intersection_enu[1] / EARTH_RADIUS
+    delta_lon_rad = intersection_enu[0] / (EARTH_RADIUS * np.cos(np.radians(cam_lat)))
+
+    lon = cam_lon + np.degrees(delta_lon_rad)
+    lat = cam_lat + np.degrees(delta_lat_rad)
+
+    return {"lon":lon, "lat":lat}
+
+# ==============================================================================
+# 使用示例
+# ==============================================================================
+if __name__ == '__main__':
+    # --- 1. 定义您最终标定好的、精确的摄像头参数 ---
+    # 这些是固定的物理参数,您应该保存下来
+    CAMERA_PARAMS = {
+        "height": 28.0000,
+        "hfov": 61.0000,
+        "resolution_w": 1280,
+        "resolution_h": 720,
+        "cam_lon": 112.893799,
+        "cam_lat": 28.221701,
+        "north_p_value": 39.0
+    }
+
+    # --- 2. 定义摄像头当前的云台状态 ---
+    # 在您的实际应用中,这些值应该是从摄像头API实时读取的
+    CURRENT_PTZ_PARAMS = {
+        "pan": 271.9,
+        "tilt": 26.2
+    }
+
+    # --- 3. 指定您想要查询的屏幕像素坐标 ---
+    pixel_u, pixel_v = 91, 276 # 使用您标定过的“黄金数据点”之一作为测试
+
+    print(f"--- 正在将像素坐标 ({pixel_u}, {pixel_v}) 转换为经纬度 ---")
+
+    # --- 4. 调用核心函数进行计算 ---
+    predicted_lon, predicted_lat = pixel_to_lonlat(pixel_u, pixel_v, CAMERA_PARAMS, CURRENT_PTZ_PARAMS)
+
+    # --- 5. 打印结果 ---
+    if predicted_lon is not None:
+        print("\n计算成功!")
+        print(f"  预测经度: {predicted_lon:.8f}")
+        print(f"  预测纬度: {predicted_lat:.8f}")
+
+        # 与标定时的真实值对比 (可选,用于验证)
+        true_lon, true_lat = 112.89461289520455, 28.221594983922852
+        print("\n--- 与标定时的真实值对比 ---")
+        print(f"  真实经度: {true_lon:.8f}")
+        print(f"  真实纬度: {true_lat:.8f}")
+    else:
+        print("\n计算失败(可能该点指向天空)。")

+ 247 - 0
video_location.py

@@ -0,0 +1,247 @@
+# -*- coding: utf-8 -*-
+"""
+最终交付版 - Python后端GeoJSON处理器 (v11.2)
+
+功能:
+- 完全基于您提供的、投影方向正确的 project_lonlat_to_pixel 函数。
+- 核心函数 process_geojson_for_frontend 直接输出一个“干净”的、可供前端JS绘制的【线段列表】。
+- 包含一个本地OpenCV预览,用于即时验证返回给前端的数据。
+"""
+import numpy as np
+from scipy.spatial.transform import Rotation as R
+import json
+
+## -------------------------------------------------------------------------- ##
+## 1. 核心算法函数 (基于您的版本)
+## -------------------------------------------------------------------------- ##
+EARTH_RADIUS = 6378137.0
+
+def lonlat_to_enu(lon, lat, center_lon, center_lat):
+    delta_lon = np.radians(lon - center_lon)
+    delta_lat = np.radians(lat - center_lat)
+    x = EARTH_RADIUS * delta_lon * np.cos(np.radians(center_lat))
+    y = EARTH_RADIUS * delta_lat
+    return np.array([x, y, 0])
+
+def project_lonlat_to_pixel(target_lon, target_lat, resolution_w, resolution_h,
+                            cam_lon, cam_lat, cam_height, p, t, hfov, north_p_value=0.0):
+    """
+    [最终正确版] 反向投影函数:从经纬度计算像素坐标(u, v)。
+    - 正确处理 cz < 0 的情况,通过创建一个方向正确但位置在无穷远的虚拟点来实现线段裁剪。
+    """
+    # 步骤 1 & 2: ... (这部分代码不变)
+    target_enu = lonlat_to_enu(target_lon, target_lat, cam_lon, cam_lat)
+    cam_pos_enu = np.array([0, 0, cam_height])
+    vec_enu = target_enu - cam_pos_enu
+    norm = np.linalg.norm(vec_enu)
+    if norm < 1e-6:
+        return None, None, False
+    vec_enu /= norm
+
+    # 步骤 3: ... (这部分代码不变)
+    p_corrected = p - north_p_value
+    r_base = R.from_euler('x', -90, degrees=True)
+    r_pan = R.from_euler('z', p_corrected, degrees=True)
+    r_tilt = R.from_euler('x', -t, degrees=True)
+    total_rotation = r_pan * r_tilt * r_base
+    inverse_rotation = total_rotation.inv()
+    vec_cam = inverse_rotation.apply(vec_enu)
+    cx, cy, cz = vec_cam
+
+    center_u = resolution_w / 2
+    center_v = resolution_h / 2
+
+    # ===================== [ 核心修正逻辑 V3.0 ] =====================
+    # 如果点在相机后方 (cz <= 0)
+    if cz <= 1e-9:
+        # 我们不再尝试投影它。
+        # 而是根据 cx, cy 的方向,在屏幕中心的基础上,向该方向延伸一个巨大的距离,
+        # 创建一个“无穷远”的虚拟点。这个点的方向是正确的。
+        # 这个巨大的数值确保了该点一定在屏幕外。
+        MEGA_DISTANCE = 1_000_000.0
+        u = center_u + cx * MEGA_DISTANCE
+        v = center_v + cy * MEGA_DISTANCE
+        return u, v, False # 这个点本身是不可见的
+    # =================================================================
+
+    # 步骤 4: (正常投影逻辑) 如果点在相机前方,则正常计算
+    f_x = resolution_w / (2 * np.tan(np.radians(hfov) / 2))
+    f_y = f_x
+
+    u = center_u + f_x * (cx / cz)
+    v = center_v + f_y * (cy / cz)
+
+    is_visible = (0 <= u < resolution_w) and (0 <= v < resolution_h)
+
+    return u, v, is_visible
+
+## -------------------------------------------------------------------------- ##
+## 2. GeoJSON解析 与 线段裁剪算法
+## -------------------------------------------------------------------------- ##
+def parse_geojson_coordinates(geojson_data):
+    coordinates = []
+    try:
+        if geojson_data.get('type') == 'FeatureCollection': geometry = geojson_data['features'][0]['geometry']
+        else: geometry = geojson_data
+        if geometry.get('type') == 'Polygon':
+            exterior_ring = geometry['coordinates'][0]
+            coordinates = [(coord[0], coord[1]) for coord in exterior_ring]
+            print(f"成功从GeoJSON中提取了 {len(coordinates)} 个边界点。")
+            return coordinates
+    except (KeyError, IndexError, TypeError): pass
+    return []
+
+INSIDE, LEFT, RIGHT, BOTTOM, TOP = 0, 1, 2, 4, 8
+def compute_outcode(x, y, xmin, ymin, xmax, ymax):
+    code = INSIDE
+    if x < xmin: code |= LEFT
+    elif x > xmax: code |= RIGHT
+    if y < ymin: code |= BOTTOM
+    elif y > ymax: code |= TOP
+    return code
+def cohen_sutherland_clip(x1, y1, x2, y2, xmin, ymin, xmax, ymax):
+    code1 = compute_outcode(x1, y1, xmin, ymin, xmax, ymax); code2 = compute_outcode(x2, y2, xmin, ymin, xmax, ymax)
+    accept = False
+    while True:
+        if not (code1 | code2): accept = True; break
+        elif code1 & code2: break
+        else:
+            x, y = 0.0, 0.0; code_out = code1 if code1 else code2
+            if code_out & TOP:
+                if y2 != y1: x = x1 + (x2 - x1) * (ymax - y1) / (y2 - y1)
+                else: x = x1
+                y = ymax
+            elif code_out & BOTTOM:
+                if y2 != y1: x = x1 + (x2 - x1) * (ymin - y1) / (y2 - y1)
+                else: x = x1
+                y = ymin
+            elif code_out & RIGHT:
+                if x2 != x1: y = y1 + (y2 - y1) * (xmax - x1) / (x2 - x1)
+                else: y = y1
+                x = xmax
+            elif code_out & LEFT:
+                if x2 != x1: y = y1 + (y2 - y1) * (xmin - x1) / (x2 - x1)
+                else: y = y1
+                x = xmin
+            if code_out == code1: x1, y1 = x, y; code1 = compute_outcode(x1, y1, xmin, ymin, xmax, ymax)
+            else: x2, y2 = x, y; code2 = compute_outcode(x2, y2, xmin, ymin, xmax, ymax)
+    if accept: return [[int(x1), int(y1)], [int(x2), int(y2)]]
+    return None
+
+## -------------------------------------------------------------------------- ##
+## 3. 核心处理函数
+## -------------------------------------------------------------------------- ##
+def process_geojson_for_frontend(geojson_data, camera_params, calibrated_params):
+    geo_coords = parse_geojson_coordinates(geojson_data)
+    if not geo_coords: return []
+    if len(geo_coords) > 1 and geo_coords[0] != geo_coords[-1]:
+        geo_coords.append(geo_coords[0]) # 确保多边形闭合
+
+    full_params = {**camera_params, **calibrated_params}
+    raw_pixel_points = []
+    # 1. Batch convert all geographic points to pixel points
+    for lon, lat in geo_coords:
+        u, v, is_visible = project_lonlat_to_pixel(
+            lon, lat,
+            resolution_w=camera_params['resolution_w'], resolution_h=camera_params['resolution_h'],
+            cam_lon=camera_params['cam_lon'], cam_lat=camera_params['cam_lat'],
+            cam_height=calibrated_params['cam_height'], p=camera_params['p'],
+            t=camera_params['t'], hfov=calibrated_params['hfov'],
+            north_p_value=camera_params['north_p_value']
+        )
+        raw_pixel_points.append([int(u), int(v)])
+
+    visible_segments = []
+    screen_w = camera_params['resolution_w']
+    screen_h = camera_params['resolution_h']
+
+    for i in range(len(raw_pixel_points) - 1):
+        p1 = raw_pixel_points[i]
+        p2 = raw_pixel_points[i+1]
+        if p1[0] == float('inf') or p2[0] == float('inf'): continue
+        u1, v1 = p1; u2, v2 = p2
+        clipped_segment = cohen_sutherland_clip(u1, v1, u2, v2, 0, 0, screen_w - 1, screen_h - 1)
+        if clipped_segment:
+            visible_segments.append(clipped_segment)
+
+    return visible_segments
+
+## -------------------------------------------------------------------------- ##
+## 4. 主程序
+## -------------------------------------------------------------------------- ##
+if __name__ == '__main__':
+    geojson_string = """
+{
+  "type": "FeatureCollection",
+  "features": [
+    {
+      "type": "Feature",
+      "properties": {
+        "name": "",
+        "id": "m-7d43a6d0-a7bd-4fc1-84b2-deab51757f8e",
+        "type": "polygon",
+        "style": {
+          "color": "#29cf34",
+          "opacity": 0.5,
+          "outline": true,
+          "outlineWidth": 3,
+          "outlineColor": "#ffffff",
+          "clampToGround": true,
+          "materialType": "Color",
+          "label": {
+            "text": "",
+            "font_size": 18,
+            "color": "#ffffff",
+            "outline": true,
+            "outlineColor": "#000000",
+            "pixelOffsetY": -10,
+            "distanceDisplayCondition": true,
+            "distanceDisplayCondition_far": 500000,
+            "distanceDisplayCondition_near": 0
+          }
+        },
+        "options": {}
+      },
+      "geometry": {
+        "type": "Polygon",
+        "coordinates": [
+          [
+            [
+              112.894539,
+              28.221327,
+              0
+            ],
+            [
+              112.894712,
+              28.221333,
+              0
+            ],
+            [
+              112.894709,
+              28.221267,
+              0
+            ],
+            [
+              112.894533,
+              28.221267,
+              0
+            ]
+          ]
+        ]
+      }
+    }
+  ]
+}
+    """
+    geojson_data = json.loads(geojson_string)
+    calibrated_params = {"cam_height": 28.0000, "hfov": 61.0000}
+    camera_params = {
+        "resolution_w": 1280, "resolution_h": 720,
+        "cam_lon": 112.893799, "cam_lat": 28.221701,
+        "north_p_value": 39.0, "p": 271.9, "t": 26.2
+    }
+
+    final_screen_segments = process_geojson_for_frontend(geojson_data, camera_params, calibrated_params)
+
+    print(json.dumps(final_screen_segments, indent=2))
+