在NVIDIA-Jetson平台上构建智能多媒体服务器

Building a Multi-Camera Media Server for AI Processing on the NVIDIA Jetson Platform

媒体服务器提供多媒体一体功能,例如视频捕获、处理、流式处理、录制,在某些情况下,还能够在某些事件下触发操作,例如自动拍摄快照。

要使媒体服务器发挥最佳性能,必须是可扩展的、模块化的,并且易于与其进程集成。一个典型的例子是通过进程间通信控制媒体服务器的GUI。

在本文中,将向展示如何在NVIDIA Jetson平台上构建一个简单的用于人工智能处理的实时多摄像机媒体服务器。将演示如何使用GStreamer守护进程(GstD)、GstInterpipe和NVIDIA DeepStream SDK开发一个可伸缩的健壮原型,以便从多个不同的视频源捕获。              除了实现实时的深度学习推断之外,服务器是完全动态的,因为可以在运行时更改正在处理的多个视频流的属性和状态。

一个有价值的附加组件是触发动作的能力,例如拍摄快照或录制视频作为对特定事件的响应。图1显示了在NVIDIA DeepStream软件模块执行一些AI处理之后,视频流的一个示例快照,其中包含生成的边界框。稍后描述的示例媒体服务器使用两个摄像机,但是,复制这两个视频流以演示可以添加多个视频流。

Figure 1. AI media server sample snapshot.

在本文的最后,将了解一组基于GStreamer的工具,以及如何根据特定的多媒体和人工智能需求来扩展。要学习本教程,应该具备GStreamer和DeepStream框架的基本知识。

Why
a media server?

在媒体服务器中使用DeepStream为智能媒体产品提供了快速上市解决方案。监视和运动流(如图2所示)是两个用例的例子,在这些用例中,可以使用增强了人工智能功能的媒体服务器来分析捕获的视频并提取有用的信息。当检测到异常行为时,服务器可以触发记录或快照等事件。

Figure 2: Media server
example for sports streaming.

AI
media server modules

人工智能媒体服务器模块

媒体服务器划分为四个模块:

视频捕获

视频处理与人工智能

视频编码

其功能,如录制、流式处理和快照。

与集成媒体服务器的每个模块相对应的四个模块:视频捕获、视频处理和人工智能、视频编码和附加功能。

如果使用GStreamer实现媒体服务器,则最终会得到如下代码示例所示的管道:

v4l2src
device=/dev/video1 ! video/x-raw,width=640,height=480 ! videoconvert !
video/x-raw,format=I420,width=640,height=480 ! queue ! tee name=camera0 \

nvarguscamerasrc
! nvvidconv ! video/x-raw,format=I420,width=640,height=480 ! queue ! tee
name=camera1 \

camera0.
! video/x-raw,format=I420,width=640,height=480 ! nvvideoconvert !
video/x-raw(memory:NVMM),format=NV12,width=640,height=480 ! nvstreammux0.sink_0
\

camera0.
! video/x-raw,format=I420,width=640,height=480 ! nvvideoconvert !
video/x-raw(memory:NVMM),format=NV12,width=640,height=480 ! nvstreammux0.sink_1
\

camera1.
! video/x-raw,format=I420,width=640,height=480 ! nvvideoconvert !
video/x-raw(memory:NVMM),format=NV12,width=640,height=480 ! nvstreammux0.sink_2
\

camera1.
! video/x-raw,format=I420,width=640,height=480 ! nvvideoconvert !
video/x-raw(memory:NVMM),format=NV12,width=640,height=480 ! nvstreammux0.sink_3
\

nvstreammux name=nvstreammux0 batch-size=4
batched-push-timeout=40000 width=640 height=480 ! queue ! nvinfer batch-size=4
config-file-path=deepstream-models/config_infer_primary_4_cameras.txt ! queue !
nvtracker ll-lib-file=deepstream-models/libnvds_mot_klt.so enable-batch-process=
true ! queue ! nvmultistreamtiler width=640 height=480
rows=2 columns=2 ! nvvideoconvert ! nvdsosd ! queue ! tee name=deep \

deep. ! nvvideoconvert ! nvv4l2h264enc
insert-sps-pps=true iframeinterval=10
! tee name=h264 \

deep. ! nvvideoconvert ! nvv4l2h265enc
insert-sps-pps=true iframeinterval=10
! tee name=h265 \

deep. ! nvvideoconvert ! nvv4l2vp9enc ! tee
name=vp9 \

deep. ! nvvideoconvert !
video/x-raw,width=640,height=480 ! nvjpegenc ! tee name=jpeg \

h264. ! h264parse ! matroskamux ! filesink
name=file location=test-h264-0.mkv \

h265. ! h265parse ! matroskamux ! filesink
name=file location=test-h265-0.mkv \

vp9. ! matroskamux ! filesink name=file
location=test-vp9-0.mkv \

jpeg. ! filesink name=file location=test-snapshot0.jpg

除了很难阅读之外,这段代码对于动态控制模块连接和管道状态也不是很简单。此外,这种方法不可扩展,可能导致代码复制,更不用说实现简单原型所需经历的耗时学习曲线了。以下是如何克服这些问题的方法:

模块互连

模块控制

Module
interconnection

一个更好的实现应该有多个更小的管道,而不是一个非常大的管道。问题是如何连接这些管道。

RidgeRun的GstInterpipe是一个开源插件,可以解决这个问题。GstInterpipe插件提供了两个元素interpipesrc和interpipesink。

Module
control

媒体服务器的一个理想特性是能够对正在处理的不同流的状态和属性进行某种程度的控制。              RidgeRun的GstD是一个开源项目,是处理GStreamer框架的多线程Linux守护进程。GstD提供了一个类似gst启动的命令行接口gst
client,以及C和Python绑定。简化了动态管道管理,加快了原型和开发时间。

Building
an AI media server in 30 minutes

所有这些都准备好了,现在可以使用这里描述的工具将所有内容放在一个真实的实现中。可以使用DeepStream、基于GStreamer的工具(GstD、GstInterpipe)和硬件加速插件,在Python中为Jetson TX2板创建一个示例媒体服务器。

Prerequisites

要运行AI媒体服务器,需要安装Jetpack 4.3的Jetson TX2板。此外,演示假设板上连接了两个摄像头,一个在MIPI CSI上,一个在USB上。

按照wiki页面安装GStreamer守护进程和GstInterpipe。安装这些开源项目后,可以按照运行演示wiki页面中的说明进行操作。

Video
capture

在这个模块中,应用程序通常使用一个或多个摄像头或捕获硬件设备,为媒体服务器提供视频输入。对于每个用例,考虑接口和硬件制造商来选择正确的解决方案。

有多种接口可用于捕获与Jetson板兼容的视频。这些选项包括MIPI
CSI、USB、SLVS、GMSL和FPD Link。有多家制造商为Jetson平台板提供摄像头,如索尼、OmniVision和OnSemi。

有些应用需要使用许多摄像头。在这种情况下,需要特殊的硬件和软件。一个例子是D3工程的子板(图3),连接到Jetson AGX Xavier上,使用RidgeRun的相机驱动程序获得最多16个FPD Link III工作相机。有关更多信息,请参见演示,RidgeRun D3 NVIDIA Partner Showcase Jetson Xavier多摄像头AI演示。

Figure 3: D3
Engineering hardware for 16 FPD-Link III cameras on Jetson AGX Xavier.

在这个媒体服务器示例中,使用两个摄像头:MIPI CSI和USB摄像头。以下是USB摄像头的捕获管道:              client.pipeline_create('camera0', 'v4l2src device=/dev/video0
! \ video/x-raw,format=YUY2,width=1280,height=720 ! \interpipesink name=camera0
forward-events=true forward-eos=true sync=false')

client对象是用于控制媒体服务器管道的GstD客户端实例。

使用管道名称和描述为参数的管道创建命令。这个camera0管道使用v4l2src元素从USB摄像机捕获,在本例中,设备属性设置为/dev/video0。此属性必须根据每个板中的视频设备排列进行设置。通过名为camera0的interpipesink实例使USB摄像头捕获可用。

现在,考虑一下用于从CSI MIPI相机捕获的camera1管道:

client.pipeline_create('camera1', 'nvarguscamerasrc ! nvvidconv !
video/x-raw,format=I420,width=1280,height=720 ! queue ! interpipesink
name=camera1 forward-events=true forward-eos=true sync=false')

要从该传感器捕获,请使用nvarguscamerasrc元素,NVIDIA视频捕获专有元素下面使用libargus。CSI MIPI camera视频流通过名为camera1的interpipesink实例提供。

Video
processing and AI

视频处理块由两个功能单元组成。一个负责调整和修改捕获的视频缓冲区(视频处理)的属性,另一个负责将原始流数据转换为可操作的视野(人工智能处理)。

视频处理

通常,原始摄像机视频流需要一些格式更改才能被其模块使用。这些更改可能与帧速率、分辨率、颜色空间或用于分配缓冲区的内存类型有关。

要执行这些操作,可以利用NVIDIA的硬件加速GStreamer元素。例如,考虑camera0_rgba_nvmm和camera1_rgba_nvmm管道,各自监听各自的相机。nvvideoconvert元素用于同时执行颜色空间、内存转换和内存:

client.pipeline_create('camera0_rgba_nvmm', 'interpipesrc listen-to=camera0 !
video/x-raw,format=YUY2,width=1280,height=720 ! videoconvert !
video/x-raw,format=NV12,width=1280,height=720 ! nvvideoconvert !
video/x-raw(memory:NVMM),format=RGBA,width=1280,height=720 ! queue !
interpipesink name=camera0_rgba_nvmm forward-events=true forward-eos=true
sync=false caps=video/x-raw(memory:NVMM), format=RGBA, width=1280, height=720, pixel-aspect-ratio=1/1,interlace-mode=progressive,framerate=30/1')

已处理的视频流由名为camera0_rgba_nvmm和camera1_rgba_nvmm的管道间接收器实例提供。             

人工智能处理             

AI处理模块完全依赖于DeepStream SDK中提供的一组基于GStreamer的工具。提供了一个可操作的洞察级别,涉及对原始数据(在媒体服务器的情况下,来自摄像机的视频缓冲区)的分析和GPU上的后续处理。可以通过DeepStream访问的一些功能包括:             

流聚合和批处理             

基于时态相关的推理用于检测、分类和分割             

目标跟踪参考实现             

用于突出显示对象和文本覆盖的屏幕显示API             

参考媒体服务器示例,interpipesrc元素侦听摄像机处理的视频流,并将其转换为运行批处理机制的nvreammux元素的四个输入。尽管在这个示例媒体服务器中仅使用两个摄像机,但可以将输入复制到nvreammux以演示可以使用更多摄像机。             

mux元素的输出进入nvinfer实例和其可用的DeepStream元素,最终到达interpipesink实例,该实例向媒体服务器的下一个阶段提供包含推断信息的视频帧。

client.pipeline_create('deepstream',
'\

interpipesrc
listen-to=camera0_rgba_nvmm ! nvstreammux0.sink_0 \

interpipesrc
listen-to=camera0_rgba_nvmm ! nvstreammux0.sink_1 \

interpipesrc
listen-to=camera1_rgba_nvmm ! nvstreammux0.sink_2 \

interpipesrc
listen-to=camera1_rgba_nvmm ! nvstreammux0.sink_3 \

nvstreammux name=nvstreammux0 batch-size=4 batched-push-timeout=40000
width=1280 height=720 ! queue ! nvinfer batch-size=4
config-file-path=../deepstream-models/config_infer_primary_4_cameras.txt !
queue ! \

nvtracker
ll-lib-file=../deepstream-models/libnvds_mot_klt.so enable-batch-process=
true ! queue ! nvmultistreamtiler width=1280 height=720
rows=2 columns=2 ! nvvideoconvert ! nvdsosd ! queue ! \

interpipesink name=deep
forward-events=
true forward-eos=true sync=false')

Video
encoding

在这一点上,有一个包含(或不包含)人工智能信息的视频缓冲流。对于其模块(如录制、流式处理或拍摄快照),需要数据压缩以避免不可管理的文件大小问题、网络过载和其一些问题。当嵌入式系统的可用硬件资源有限且功耗是关键竞争因素时,这些问题尤其重要。

编码和解码是一种资源密集型操作。当接收到重要的数据流时,这可能会导致处理瓶颈。这个限制可以通过使用基于硬件的编码器和解码器(编解码器)来解决。NVIDIA提供了硬件编解码器,可以在专门的硬件上加速编码和解码,从而为其任务卸载CPU和GPU单元。

正在构建的媒体服务器描述了两种不同的视频编码格式(H.264和VP9)选项和一种图像编码(JPEG)选项。

下面的代码示例显示了H.264、VP9和JPEG的这种编码管道的实现:

client.pipeline_create('h264',
'interpipesrc name=h264_src format=time listen-to=deep !
video/x-raw(memory:NVMM),format=RGBA,width=1280,height=720 ! nvvideoconvert !
nvv4l2h264enc ! interpipesink name=h264_sink forward-events=true
forward-eos=true sync=false async=false enable-last-sample=false drop=true')

client.pipeline_create('vp9',
'interpipesrc name=vp9_src format=time listen-to=deep ! nvvideoconvert !
nvv4l2vp9enc max-perf=true ! interpipesink name=vp9_sink forward-events=true
forward-eos=true sync=false async=false enable-last-sample=false
drop=true')

client.pipeline_create('jpeg',
'interpipesrc name=src format=time listen-to=deep ! nvvideoconvert !
video/x-raw,format=I420,width=1280,height=720 ! nvjpegenc ! interpipesink
name=jpeg forward-events=true forward-eos=true sync=false async=false
enable-last-sample=false drop=true')

前面描述的管道处理DeepStream模块的输出。通过nvvideoconvert将缓冲区转换为适合编码器的颜色空间,并使用nv4l2h264enc、nv4l2vp9enc和nvjpegenc(硬件加速)元素压缩数据。最后,通过管道将编码的缓冲区传递给以下媒体服务器模块使用的interpipesink实例。

在vp9管道中,nv4l2vp9enc元素的属性max perf设置为true。这将启用编码器的高性能模式,并且在使用高分辨率和帧速率时非常有用。但是,启用此属性会导致功耗增加,这需要在移动系统上加以考虑。

Additional
features

最后一个阶段可以称为最终产品模块。可以生成视频记录、通过网络发起视频流,或者简单地从照相机生成其中一个视频流的快照。所有选项都包括在DeepStream模块中完成的先前AI处理。

同样,每个管道都有一个listen to属性设置到每个对应的编码流。两个视频录制管道都使用Matroska容器来生成生成的文件。

H264录音

client.pipeline_create('record_h264', 'interpipesrc format=time allow-renegotiation=false
listen-to=h264_sink ! h264parse ! matroskamux ! filesink
name=filesink_record_h264')

在H.264管道的情况下,需要h264parse元素使编码块的输出适应matroskamux接受的内容。

VP9录制

client.pipeline_create('record_vp9', 'interpipesrc format=time listen-to=vp9_sink !
matroskamux ! filesink name=filesink_record_vp9')

同样,VP9也被打包到Matroska容器中。另一方面,不需要解析器。

快照

client.pipeline_create('snapshot', 'interpipesrc format=time listen-to=jpeg
num-buffers=1 ! filesink name=filesink_snapshot')

快照管道使用num buffers属性仅接受启用快照数据路径后通过interpipesrc实例的第一个缓冲区。捕获第一个缓冲区后,会自动发送一个流结束(EOS事件,以避免用更新的事件覆盖所需的缓冲区)。

网络流媒体

流协议(UDP、TCP、RTSP和WebRTC)的实际选择取决于用例场景需求和参数,如延迟、质量、安全性和成本等。

WebRTC和RTSP是最常用的流媒体解决方案,GStreamer支持这两种协议。为了加快开发速度,RidgeRun提供了GstWebRTC和GstRtspSink插件等产品。GstWebRTC用于将管道转换为与WebRTC兼容的端点,而GstRtspSink则加速原型制作并促进集成。

AI media
server dynamics

在前面的章节中,展示了AI媒体服务器实现的一些细节。现在,将探索如何控制这些模块,并利用GstInterpipes更改连接。

要播放管道,请使用命令pipeline\u play,并将管道名称用作参数。例如,要从两个摄像机开始捕获,并在媒体服务器中启动视频处理和DeepStream管道,请使用以下代码示例:

client.pipeline_play('camera0')

client.pipeline_play('camera1')

client.pipeline_play('camera0_rgba_nvmm')

client.pipeline_play('camera1_rgba_nvmm')

client.pipeline_play('deepstream')

记录

要开始录制,首先需要设置文件的写入位置。为此,请使用element\u set命令设置filelink元素的location属性,如下所示:

client.element_set('record_vp9', 'filesink', 'location', 'test_record_vp9_0.mkv')

该位置可以设置为绝对路径,也可以像前面的示例一样设置为相对路径。在位置设置为相对路径的情况下,文件将写入GtsD启动的目录。

现在可以播放编码和录制管道以开始录制过程。例如,以下命令开始录制vp9:

client.pipeline_play('vp9')

client.pipeline_play('record_vp9')

要停止录制,请将EOS事件发送到编码器管道以允许编码器正确完成,等待管道完成对任何缓冲数据的处理,然后停止编码和录制管道:

client.event_eos('vp9')

client.bus_filter('vp9',
'eos')

client.bus_read('vp9')

client.pipeline_stop('vp9')

client.pipeline_stop('record_vp9')

在某些情况下,更改编码器视频源会很有用。例如,如果要从相机输出(无对象检测)而不是DeepStream输出中录制一个常规视频流,这将非常有用。GstInterpipes允许执行此动态更改。

使用element_set命令将编码管道的listen to属性更改为提供要录制的视频流的interpipesink的名称。例如,要仅录制camera1输出,请连接到该相机的视频处理管道interpipesink,如下所示:

client.element_set('vp9',
'vp9_src', 'listen-to', 'camera0_rgba_nvmm')

然后,按照设置所需文件名和播放编码和录制管道的相同步骤启动该过程

client.element_set('record_vp9',
'filesink', 'location', 'test_record_vp9_1.mkv')

client.pipeline_play('vp9')

client.pipeline_play('record_vp9')

快照

拍摄快照与前面描述的录制类似。要拍摄快照,必须设置快照的写入位置。然后可以启动编码和快照管道。

client.element_set('snapshot',
'filesink', 'location', 'test_snapshot_0.jpeg')

client.pipeline_play('jpeg')

client.pipeline_play('snapshot')

与记录连接更改类似,快照源也可以更改。例如,这将允许拍摄单个相机捕获的快照。只需更改JPEG管道interpipesrc listen to属性

client.element_set('jpeg',
'jpeg_src', 'listen-to', 'camera0_rgba_nvmm')

复杂的场景

GtsD的灵活性允许媒体服务器根据AI模块检测到的事件触发操作。例如,检测到危险区域中的人员可能会触发媒体服务器中的信号。自定义应用程序可以监听这些类型的事件,并使用这些信息触发媒体服务器中的操作,例如拍摄快照,甚至在媒体服务器域之外执行进一步的操作。

总结

现在已经学会了创建一个媒体服务器解决方案,可以从多个摄像头捕获视频,通过人工智能处理应用一些基本级别的图像识别,并生成一个文件(视频或照片),而无需从Gstreamer和Glib学习曲线导出麻烦。在开发过程中,了解了一些工具(GstD、GstInterPipe),这些工具可以为设计带来灵活性和动态性,允许对媒体服务器中的几个数据处理分支进行运行时控制。

在NVIDIA-Jetson平台上构建智能多媒体服务器的更多相关文章

  1. NVIDIA DeepStream 5.0构建智能视频分析应用程序

    NVIDIA DeepStream 5.0构建智能视频分析应用程序 无论是要平衡产品分配和优化流量的仓库,工厂流水线检查还是医院管理,要确保员工和护理人员在照顾病人的同时使用个人保护设备(PPE),就 ...

  2. 龙芯GO!龙芯平台上构建Go语言环境指南

    龙芯软件生态系列——龙芯GO!龙芯平台上构建Go语言环境指南2016-07-05 龙芯中科1初识Go语言Go语言是Google公司于2009年正式推出的一款开源的编程语言,是由Robert Gries ...

  3. Jexus是一款Linux平台上的高性能WEB服务器和负载均衡网关

    什么是Jexus Jexus是一款Linux平台上的高性能WEB服务器和负载均衡网关,以支持ASP.NET.ASP.NET CORE.PHP为特色,同时具备反向代理.入侵检测等重要功能.可以这样说,J ...

  4. 在windows平台上构建自己的PHP(php5.3+)

    这是一篇翻译的文章,原文参见:https://wiki.php.net/internals/windows/stepbystepbuild 顺便提一句,wiki.php.net有很多精彩的内容,想深入 ...

  5. Linux DNS分离解析与构建智能DNS服务器

    一 构建DNS分离解析 方法一 : [root@localhost ~]# vim /etc/named.conf [root@localhost ~]# cd /var/named/ [root@l ...

  6. Linux上构建一个RADIUS服务器详解

    作为一名网络管理员,您需要为您所需管理的每个网络设备存放用于管理的用户信息.但是网络设备通常只支持有限的用户管理功能.学习如何使用Linux上的一个外部RADIUS服务器来验证用户,具体来说是通过一个 ...

  7. 在windows平台上构建自己的PHP(仅适用于php5.2)

    构建步骤 1, 安装vs2008 2, 安装windows sdk 6.1 3, 下载php 5.2源码,可以从此处获取Releases(先不要解压) 4, 下载bindlib_w32.zip,htt ...

  8. 使用Botkit和Rasa NLU构建智能聊天机器人

    欢迎大家前往云+社区,获取更多腾讯海量技术实践干货哦~ 我们每天都会听到关于有能力涉及旅游.社交.法律​​.支持.销售等领域的新型机器人推出的新闻.根据我最后一次查阅的数据,单单Facebook Me ...

  9. 在Jetson TX2上显示摄像头视频并使用python进行caffe推理

    参考文章:How to Capture Camera Video and Do Caffe Inferencing with Python on Jetson TX2 与参考文章大部分都是相似的,如果 ...

随机推荐

  1. 【SpringBoot】SpringBoot2.x整合定时任务和异步任务处理

    SpringBoot2.x整合定时任务和异步任务处理 一.项目环境 springboot2.x本身已经集成了定时任务模块和异步任务,可以直接使用 二.springboot常用定时任务配置 1.在启动类 ...

  2. 织梦DedeCMS自定义表单diy_list.htm

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...

  3. Social engineering tookit 钓鱼网站

    目录 Set 钓鱼攻击 网站克隆 Set Set(Social engineering tookit)是一款社会工程学工具,该工具用的最多的就是用来制作钓鱼网站. Kali中自带了该工具. 钓鱼攻击 ...

  4. Java Web中间件

    目录 中间件 常见的web中间件有哪些 Tomcat Weblogic Jboss Jetty Webshere Glasshfish 中间件 我们经常会看到中间件,但是,一直好奇的是,中间件到底是什 ...

  5. CreateThread 线程操作与 _beginthreadex 线程安全(Windows核心编程)

    0x01 线程的创建 线程不同于进程,Windows 中的进程是拥有 '惰性' 的,本身并不执行任何代码,而执行代码的任务转交给主线程,列如使用 CreateProcess 创建一个进程打开 Cmd ...

  6. 浅谈Java中的公平锁和非公平锁,可重入锁,自旋锁

    公平锁和非公平锁 这里主要体现在ReentrantLock这个类里面了 公平锁.非公平锁的创建方式: //创建一个非公平锁,默认是非公平锁 Lock lock = new ReentrantLock( ...

  7. 【Mybtais】Mybatis 插件 Plugin开发(一)动态代理步步解析

    需求: 对原有系统中的方法进行'拦截',在方法执行的前后添加新的处理逻辑. 分析: 不是办法的办法就是,对原有的每个方法进行修改,添加上新的逻辑:如果需要拦截的方法比较少,选择此方法到是会节省成本.但 ...

  8. 远程连接mysql出现"Can't connect to MySQL server 'Ip' ()"的解决办法

    1.大多是防火墙的问题(参考链接:https://blog.csdn.net/jiezhi2013/article/details/50603366) 2.上面方法不能解决,不造成影响情况下可关闭防火 ...

  9. 附近的人?你zao吗?

    前几天收到一个新的需求,需要实现类似"附近的人"的功能:根据自己当前的定位,获取距离范围内的所有任务地点.刚看到这个需求时有点懵逼,第一想到的就是要利用地球的半径公式去计算距离,也 ...

  10. k3d入门指南:在Docker中运行K3s

    在本文中,我们将简单了解k3d,这是一款可让您在安装了Docker的任何地方运行一次性Kubernetes集群的工具,此外在本文中我们还将探讨在使用k3d中可能会出现的一切问题. 什么是k3d? k3 ...