通过AIS实现船舶追踪与照射

news2025/3/1 9:31:50

前些天突然接到个紧急的项目:某处需要实现对夜航船只进行追踪并用激光灯照射以保障夜航安全。这个项目紧急到什么程度呢?!现场激光灯都安装好了,还有三个星期就要验收了,但上家没搞定就甩给我们了:(

从技术上看,本项目没什么好写的,但正因为本项目如此之紧急,所以在工程上还有点意思,所以总结一下,以资参考。

系统组成

说起来,整个项目并不复杂。我完成的系统图如下:
在这里插入图片描述
其中:

一、现场采集器就是用之前在rust嵌入式开发之总结一文介绍过的我司标准板卡,主要完成:

  • 到两激光灯的RS485连接,并以Pelco-D球机控制协议进行控制
  • 和服务器的通信,实现远程控制
  • 提供命令行接口,完成激光灯的坐标读取等工程测试、标定等基础工作

本还需要完成到AIS基站的RS232串口连接来实时获取AIS数据,但AIS基站已经安装完毕,上家却没和设备厂家说如何取数据的事,所以AIS厂家也就没留RS232接口给我们,好在AIS厂家还提供了TcpServer能力,所以从AIS基站读取AIS数据的功能移到了应用服务器处。

二、应用服务器就是我之前介绍过的docker版TMS系统,有全套的业务快速开发、数据采集与处理能力。包括了两块:

  • 业务系统:java开发的tms业务后台,可以提供业务管控应用的低成本快速定制。本项目不需要向客户提供业务管控与应用,但需要制作几个测试操作界面,如模拟定位【即通过页面输入经纬度坐标,分别控制两灯的转动】、两灯的手动开关、开启关闭几个相关的debug能力以辅助调试、工程数据采集和验收

参考:jxTMS的设计思想

  • 数据系统:python开发的数据采集、解析、保存、处理系统,提供了完整的数据处理框架,可以快速而低成本的实现数据的处理与分析

参考:使用jxTMS采集数据之一

基于这两个系统,本项目一共只写了5个py文件,分别是:

1、site_ais_trace.py,在数据处理框架中定制一个站点。除站点的通用功能外【如设备管理、设备通信与远程控制等】,主要是提供激光灯的控制。代码非常简单,主要就是两个对象函数:

from module.ais_trace import ship, light, light_init
class site_ais_trace(site_packet):
	def __init__(self, name):
		#将激光灯的控制函数设置为本对象的control函数
		light.set_control_func(self.control)
		#继承父类
		super(site_ais_trace,self).__init__('site_ais_trace', name)
		#对激光灯进行初始化
		light_init()

    def control(self, slave=1, x=None, y=None, light=None):
	    #控制功能也非常简单,就是向现场采集器下达三个远程控制命令
	    #激光灯的控制是实现在现场控制器的laser_lamp_control函数中的
	    #该函数为命令行和远程控制所共用
        rd = {'cmd':'laser_lamp_control'}
        if not x is None:
	        #控制激光灯水平转动
	        #根据设备手册,指定水平转动的值
            jxUtils.checkAssert(0 <= x and x <= 35999, 'x must in [0, 35999]')
            #根据laser_lamp_control函数的要求来设置参数
            rd['s'] = slave
            rd['n'] = 'x'
            rd['v'] = x
            #将控制命令下达给本站对应的现场控制器
            rs = self.systemCmd(rd)
            jxGo.log('info', f'site_ais_trace::control[{slave}]  x:{x} req:{rs}')
            time.sleep(0.05)
        if not y is None:
	        #控制激光灯俯仰转动
            jxUtils.checkAssert(0 <= y and y <= 8999 or 27000 <= y and y <= 35999, 'y must in [0, 8999] or [27000, 35999]')
            rd['s'] = slave
            rd['n'] = 'y'
            rd['v'] = y
            rs = self.systemCmd(rd)
            jxGo.log('info', f'site_ais_trace::control[{slave}]  y:{y} req:{rs}')
            time.sleep(0.05)
        if not light is None:
	        #控制激光灯灯光的开启与关闭
            jxUtils.checkAssert(light == 'close' or light == 'open', 'light must in open|close')
            rd['s'] = slave
            rd['n'] = 'light'
            rd['v'] = light
            rs = self.systemCmd(rd)
            jxGo.log('info', f'site_ais_trace::control[{slave}]  light:{light} req:{rs}')
            time.sleep(0.05)

2、device_ais_trace.py,在数据处理框架中定制一个设备,用来从ais采集数据。主要就是设备的标准功能【如设备数据分析、保存、设备活跃性检测等】。代码同样非常简单,主要就是两个对象函数:

from module.tcpClient import tcpClient
from module.ais_trace import ship
#导入AIS解析
from ais.aismParser import aismParser

#创建一个AIS解析器
_aismParser = aismParser()
class device_ais_trace(device):
    def __init__(self, name, mySite, conf):
	    #AIS设备的手动配置工作:不执行超时检查、每条数据都保存等
        conf['timeOut'] = 0
        conf['timeOutCheckInterval'] = 0
        conf['saveDataInterval'] = 0
        conf['dataType'] = 'aism'
        #不将接收到的数据发送到数据总线上
        conf['dataBusInform'] = False
        super(device_ais_trace,self).__init__('device_ais_trace', name, mySite, conf)
        #本设备的数据不是标准的获取方法【通过现场采集器采集并发回】,需要用tcpClient手动获取
        self.client = tcpClient(self.recv, ip='xxx.xxx.xxx.xxx', port=pppp)
        self.client.connect()

    def recv(self, data):
	    #解析读取到的AIS数据
        str = data.decode('utf-8')
        l, rc = _aismParser.receive(str)
        if rc:
            for d in l:
	            #交ais_trace进行跟踪与照射的处理
                rd = ship.check(d)
                #如果需要照射,则保存接收到的AIS数据以备复查
                #由于该点的AIS基站天线配置的较强大,会接收到周围数十公里的AIS数据
				#该地是较大的港口,接收到的AIS数据量非常大,所以丢弃和本项目无关的AIS数据
                if not rd is None:
                    d['light'] = rd
                    #设备的receive函数是数据处理框架的标准接收接口,会自动完成:
                    #设备状态检测、数据保存、数据广播【供其它感兴趣的应用使用】等工作
                    self.receive(d)

3、main.py,数据系统主入口函数,主要完成系统启动、系统装配等工作。具体代码为:

#命令行处理
sn, sa = jxUtils.init()
#站点配置【下述的站点配置和站点订阅,也可以用web界面操作完成】
sc = {}
sc['type'] = 'site_ais_trace'
sc['name'] = 'xxx_站点名_xxx'
#设备列表
dl = []
sc['devices'] = dl
#添加设备配置
#本项目就一个ais设备
d = {}
d['type'] = 'device_ais_trace'
d['name'] = 'xxx_设备名_xxx'
dl.append(d)
#将站点添加到系统中
site.addSite(sc)
#通过mqtt订阅该站点以实现和站点的数据收发
from jx.jxUtils import _mqttClient
_mqttClient.subscribe('xxx_站点名_xxx')

#启动一个pyService完成数据系统和业务系统的勾连,这样就可以通过业务系统中定制的web界面向数据系统下达命令了
jxUtils.startSlave(sn,sa)

注:一般用脚本来启动main.py,这样可以通过灵活的设置命令行参数来实现各种系统配置。本系统的启动脚本:

cd /home/tms/python/
python3 main_slave.py -n 'xxx_项目名' --startDeviceDataQuery --mqServerIP '127.0.0.1' --serviceName 'cwz01' --dataBus --dbName 'demoOrg_2255' --site --app --module --mqttServerIP '127.0.0.1' &

4、module/service_ais_trace.py,向标准的pyService处理框架中插入几个控制命令,实现业务系统对数据系统的命令,如在web界面直接下达开关灯命令:

from jx.mainService import mainService
from module.ais_trace import _light_1, _light_2

def lightControl(params):
    jxGo.log('info',f'lightControl recv:{params}')
    slave = params.get('slave', 1)
    active = params.get('active', False)
    if slave == 1:
        if active:
            _light_1.light_open()
        else:
            _light_1.light_close()
    if slave == 2:
        if active:
            _light_2.light_open()
        else:
            _light_2.light_close()
            
mainService.register('lightControl',lightControl)

这样就实现了通过web界面进行灯光开关的控制功能,在调试阶段非常的方便,需要增加什么样的调试功能,就可以非常迅速的实现了。

5、module/ais_trace.py,是本项目专门开发的ais跟踪计算模块,是本项目的核心算法模块。后文再展开介绍其处理逻辑

三、AIS基站启动tcpServer来供应AIS数据;两激光灯【附带一个摄像头,两者安装在同一个云台上】则是受控设备,接受ais_trace计算出来的x轴、y轴坐标,然后执行相应的旋转与俯仰、开关灯等动作。

AIS跟踪的基本原理

通过AIS数据来追踪船只位置,原理非常简单:

1、用AIS解析器来解析船只发送的AIS数据,得到船只的经纬度数据

2、根据经纬度计算出船只到本站的距离,从xxxx米开始追踪,到警戒区即开始照射【白天不照射】,警戒区划定为yyy米

注:追踪、照射的两阶段接力处理算法,是原本考虑当AIS数据发送频率太低时,通过扩大追踪范围尽可能多的收集船只位置数据,然后拟合出船只的航迹,再通过船速计算出船只下一刻的可能位置进行补点,以实现较好的照射效果。但到现场后,由于需要保障的夜航距离非常近,这一补点算法没有实施的必要,所以取消。但跟踪、控制的两阶段算法已经没时间进行重构了,只能保留

3、当船只进入警戒区后,两灯分别根据船只经纬度计算船只到灯的水平面【x轴】的张角和垂直面【y轴】的俯仰角,然后将其映射到激光灯坐标系下,再换算成激光灯对应的控制数据,最后发送给现场控制器下达到两灯执行

4、由于该站为旅游项目,激光灯下方各处都存在人员行走和休息区域,考虑到强光对人眼的威胁,所以有必要对灯光的照射范围进行限制,当计算出的【x, y】超出许可范围时,不照射

所以,本项目的核心就是地球平面直角坐标系中的各种三角函数的运算。主要涉及:

  • 经纬度转换
  • 根据经纬度计算两点间距离
  • 根据经纬度计算某点和本点的张角
  • 坐标系旋转
  • 不同坐标系的映射与变换

上述这些计算,只要大家初高中时三角函数学的还行,然后网上搜搜就完全可以写出来了。这里就不复赘述了。

有必要说明的只有一点,即:python的浮点数的精度问题。

python中常用的浮点数的精度在15-17位左右。而地球上经度一度差不多就是111公里,即1米就需要精确到百万分之九;地球半径我们取的是:6371004米,加上运算所需,所以浮点数的精度可能是不太够。

如果大家担心这一点,选择了Decimal,那就尽量设的大一点。

当然,就本项目来说,由于其它方面的误差太大,浮点数精度不够的影响可以忽略。

AIS跟踪的工程基准

本项目有4类7个基准数据,需要通过工程的方法加以确定。

这4类基准数据,是本项目保证追踪效果的基石:

  • 本站基准经纬度:用来确定船只和本站的距离,超过一定距离的船只就不需要跟踪了。本数据可以在本站主要位置选点,精度要求也不需要太高,反正误差几米不过是将原本可以不跟踪的船也纳入跟踪而已,这点计算量不值一提
  • 两灯的安装点经纬度:这是实现控制灯水平面旋转到船只所在方位的基础数据,自然是要求精度尽可能的高。但由于项目太紧急了,我的准备工作也自然不充分,没有借到差分的GPS设备。这种情况下经过和自己用手机打点的比较后,就选用了经过甲方确认的工程施工资料中的两灯经纬度【工程上的甩锅基本律:不准那是因为甲方提供的基准点数据的问题:)】
  • 两灯的海拔高度:这是实现控制灯垂直面俯仰,使得灯对照船只的基础数据。确定起来也简单,就以甲方提供的工程资料中所标注的平台海拔加上灯头到平台的高度作为两灯的海拔高度就可以了
  • 两灯安装后各灯的x轴零点朝向:这是完成计算后,将计算结果从经纬度坐标变换成激光灯的x轴坐标的基础数据。很遗憾,我们只有手机上的各种罗盘应用,这些应用都只给到度,简直郁闷至死!

其实,还有两灯的安装面的水平倾角这个基准数据。但原则上,我们肯定是要求水平安装的,而这在工程施工时也是最好矫正的。

但很遗憾,我看到激光灯的时候,某个灯的水平倾角肉眼可见的倾斜了,起码达到了十几度!而我是周一上去的,周五就要验收,还是台风天气,时不时的就风雨大作,大风天气下登高工作太过危险,所以干脆放弃了对两灯的安装面进行校准的想法。

这时,我们就面临两个关键的工程校准工作,这两者将直接决定项目成败:

  • 两灯的x轴零点朝向如何校准?
  • 两灯的水平倾角如何纠正?

既然手机罗盘只能达到度的精度,反正都已经是不尽如人意了,那干脆就用人眼视觉效果进行校准好了。

所以我的办法非常粗暴:

  • 在平台上选择了几个非常醒目的标识点,打这些点的GPS经纬度
  • 在不进行x轴校准的情况下计算出灯的x轴旋转度数
  • 手动将摄像头旋转到标识点,一点一点的精细操作摄像头对正该标识点
  • 从现场控制器通过命令行直接读取到此时激光灯的x轴数值

此时,直接读取到的x值和计算出的x值的差,就是激光灯x轴零点朝向的校准值。只要将几个标识点的校准值一一得到,就可以得到一个工程上比较满意的x轴零点朝向的校准值了。

但是,那个水平倾角误差较大的灯就会有很明显的误差:根据某点得到的x轴零点朝向的校准值,算出来的其它点的x值的误差最大的能达到25度!!这时看到的效果,就是船只没有被套住,即激光灯旋转过去后,船只不在画面内。

由于水平倾角无法归零,所以最终我选择了最重要的航线为基准剖面,以该面的x轴零点朝向的校准值作为最终值来设定激光灯的基准数据。

至于激光灯【x, y】值的范围约束,只要旋转激光灯,避开有人区域后,通过现场控制器的控制台读取此时的x、y值即可,然后根据一系列的【x, y】,取其中的最大最小值,就能确定【x, y】值的范围约束了。

AIS跟踪的程序逻辑

通过上面的讨论,实现AIS追踪的逻辑是比较简单的,只实现了两个类:ship和light,分别代表航经的船只、两个需要控制的激光灯。

ship主要是接收到AIS数据后完成:

  • 计算船只到本站的距离,超过追踪距离的就丢弃
  • 更新ship对象的实时信息【经纬度、距离、航速、驶向还是驶离、计算出的航迹等等】
  • 请求两灯计算是否需要照射本船
  • 当船只驶离本站后,经过一段时间后将其删除,以避免数据积累耗光内存

light主要是接收到船只的照射请求后完成:

  • 检查是否需要照射,不在照射时间内就驳回请求
  • 根据船只的经纬度计算船只到本灯的距离,超过警戒距离的驳回请求
  • 根据经纬度计算船只到本灯的张角,并将计算结果变换到激光灯的水平坐标系
  • 根据上述得到的本灯x轴零点朝向计算出纠正后的x值
  • 如果x值超范围,则驳回请求
  • 根据船只到本灯的距离计算出本灯到船只的俯仰角,然后换算成激光灯的y值
  • 如果y值超范围,则驳回请求
  • 根据计算出的【x, y】值下达照射指令

这里唯一需要额外考虑的就是:在航迹上最后一个允许照射点下达照射指令后,有可能就不再发出控制指令了,不可能让激光灯就这么一直照射着。所以,在下达照射指令后,必须启动一个防呆处理:如果过几分钟还没收到新的控制指令,就直接关灯。

结语

本项目从软件编写的角度来看非常简单,但由于工程上的各种坑,所以需要综合应用多种工程手段进行应对来保证照射效果。

正应了笔者反复说的那句话:程序员首先是一个工程师,工程师就是要解决问题的,而解决问题要靠我们经过长期训练所掌握的一整套的方法论

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1905702.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Python】已解决:xml.parsers.expat.ExpatError: no element found: Line 1, column 0

文章目录 一、分析问题背景二、可能出错的原因三、错误代码示例四、正确代码示例五、注意事项 已解决&#xff1a;xml.parsers.expat.ExpatError: no element found: Line 1, column 0 一、分析问题背景 在使用Python的xml.parsers.expat模块解析XML文件时&#xff0c;有时会…

咬文嚼字:词元是当今生成式人工智能失败的一个重要原因

生成式人工智能模型处理文本的方式与人类不同。了解它们基于"标记"的内部环境可能有助于解释它们的一些奇怪行为和顽固的局限性。从 Gemma 这样的小型设备上模型到 OpenAI 业界领先的 GPT-4o 模型&#xff0c;大多数模型都建立在一种称为转换器的架构上。由于转换器在…

Java中读写文件内容乱码/BufferedReader读文件内容乱码/OutputStreamWriter设置编码集

1、问题概述&#xff1f; 在项目中我们经常会将例如日志信息放入到txt(任意后缀)文档中&#xff0c;然后在项目中通过弹框等形式查看直接的查看这些文档中的信息&#xff0c;然后有时候会出现乱码的情况。 这个时候我们就需要设置写入和写出时候的编码集情况。 2、解决方案 …

【PB案例学习笔记】-29制作一个调用帮助文档的小功能

写在前面 这是PB案例学习笔记系列文章的第29篇&#xff0c;该系列文章适合具有一定PB基础的读者。 通过一个个由浅入深的编程实战案例学习&#xff0c;提高编程技巧&#xff0c;以保证小伙伴们能应付公司的各种开发需求。 文章中设计到的源码&#xff0c;小凡都上传到了gite…

Windows使用nxlog发送系统日志到Linux的rsyslog服务器

Windows使用nxlog发送系统日志到Linux的rsyslog服务器 前言一、IP地址规划及示意图二、在windows上安装及配置nxlog1.下载nxlog2.安装nxlog3.配置nxlog4.创建对应日志路径的文件夹 三、windows上启动nxlog服务四、在CentOS 7上配置日志存到指定位置文件1.编辑/etc/rsyslog.conf…

Python | Leetcode Python题解之第222题完全二叉树的节点个数

题目&#xff1a; 题解&#xff1a; # Definition for a binary tree node. # class TreeNode: # def __init__(self, val0, leftNone, rightNone): # self.val val # self.left left # self.right right class Solution:def countNodes(self,…

[C++][ProtoBuf][初识ProtoBuf]详细讲解

目录 1.序列化概念2.ProtoBuf是什么&#xff1f;3.ProtoBuf使用特点4.补充1.GOOGLE_PROTOBUF_VERIFY_VERSION 宏2.ShutdownProtobufLibrary()3.--decode 5.序列化能力对比验证6.总结 1.序列化概念 序列化&#xff1a;把对象转换为字节序列的过程&#xff0c;称为对象的序列化反…

Java中获取Class对象的三种方式

Java中获取Class对象的三种方式 1、对象调用getClass()方法2、类名.class的方式3、通过Class.forName()静态方法4、总结 &#x1f496;The Begin&#x1f496;点点关注&#xff0c;收藏不迷路&#x1f496; 在Java中&#xff0c;Class对象是一个非常重要的概念&#xff0c;它代…

【carla】ubuntu安装carla环境

我们可以通过查看 CARLA 的 GitHub release 页面来找到最新版本的下载链接。 下载 CARLA 压缩包 访问 CARLA Releases 页面&#xff1a; CARLA Releases on GitHub 查找最新版本&#xff1a; 找到最新的版本&#xff0c;点击下载&#xff0c;第一个压缩包 3. 解压 CARLA 包&…

Python 爬虫 tiktok关键词搜索用户数据信息 api接口

Tiktok APP API接口 Python 爬虫采集Tiktok数据 采集结果页面如下图&#xff1a; https://www.tiktok.com/search?qwwe&t1706679918408 请求API http://api.xxx.com/tt/search/user?keywordwwe&count10&offset0&tokentest 请求参数 返回示例 联系我们&…

Python | Leetcode Python题解之第221题最大正方形

题目&#xff1a; 题解&#xff1a; class Solution:def maximalSquare(self, matrix: List[List[str]]) -> int:if len(matrix) 0 or len(matrix[0]) 0:return 0maxSide 0rows, columns len(matrix), len(matrix[0])dp [[0] * columns for _ in range(rows)]for i in…

仿qq音乐播放微信小程序模板源码

手机qq音乐应用小程序&#xff0c;在线音乐播放器微信小程序网页模板。包含&#xff1a;音乐歌曲主页、推荐、排行榜、搜索、音乐播放器、歌单详情等。 仿qq音乐播放微信小程序模板源码

量产工具一一页面系统(五)

目录 前言 一、产品页面数据结构抽象 1. page_manager.h 二、产品页面管理器 1.page_manager.c 三、产品页面运行 1.main_page.c 四、单元测试 1.page_test.c 2.上机测试 前言 前面我们实现了显示系统框架&#xff0c;输入系统框架&#xff0c;文字系统框架和UI系统…

WAIC热点聚焦|新质生产力与低空经济

WAIC热点聚焦|新质生产力与低空经济 概览 # WAIC热点聚焦 | 新质生产力与低空经济## 1. 新质生产力定义与特点 - 新质生产力是在新的经济社会发展阶段中形成的&#xff0c;具有变革性和高增长潜力的生产能力。## 2. 低空经济概念与构成 ### 2.1 低空经济定义 - 低空经济是依托…

斯坦福CS224n深度学习培训营课程

自然语言处理领域的经典课程涵盖了从基础知识到最新研究的全面内容。本培训营将精选课程内容&#xff0c;结合实际案例和项目实践&#xff0c;带领学员深入探索自然语言处理的前沿&#xff0c;学习最先进的深度学习技术。 课程大小&#xff1a;2.6G 课程下载&#xff1a;http…

仿哔哩哔哩视频app小程序模板源码

仿哔哩哔哩视频app小程序模板源码 粉色的哔哩哔哩手机视频网页&#xff0c;多媒体视频类微信小程序ui前端模板下载。包含&#xff1a;视频主页和播放详情页。 仿哔哩哔哩视频app小程序模板源码

Linux运维:mysql主从复制原理及实验

当一台数据库服务器出现负载的情况下&#xff0c;需要扩展服务器服务器性能扩展方式有向上扩展&#xff0c;垂直扩展。向外扩展&#xff0c;横向扩展。通俗的讲垂直扩展是将一台服务器扩展为性能更强的服务器。横向扩展是增加几台服务器。 主从复制好比存了1000块钱在主上&…

如何在 PostgreSQL 中实现数据的去重操作,尤其是对于复杂的数据结构?

文章目录 一、基本数据类型的去重二、多列数据的去重三、复杂数据结构的去重&#xff08;一&#xff09;数组类型的去重&#xff08;二&#xff09;JSON 类型的去重&#xff08;三&#xff09;结构体类型&#xff08;复合类型&#xff09;的去重 四、使用 GROUP BY 进行去重五、…

102.二叉树的层序遍历——二叉树专题复习

迭代方式&#xff1a; class Solution {// 定义一个成员变量res来存储层序遍历的结果List<List<Integer>> res new ArrayList<>();// levelOrder方法是层序遍历的接口&#xff0c;它接受一个二叉树的根节点rootpublic List<List<Integer>> lev…