语音信号基本概念– 采样率、采样深度和比特率

(1) 采样率/采样频率

我们经常听到的第一个术语是采样率或采样频率,两者指的是同一件事。你可能遇到过的一些数值是8kHz、44.1kHz和48kHz。究竟什么是音频文件的采样率?

采样率是指每秒钟记录的音频样本数。它是以每秒的样本或赫兹(缩写为Hz或kHz,1kHz为1000Hz)来衡量。一个音频样本只是一个数字,代表在一个特定时间点的测量声波值。非常重要的一点是,这些样本是在一秒钟内时间上相等的时刻采集的。例如,如果采样率是8000赫兹,那么在一秒钟内有8000个采样是不够的;它们必须在一秒钟的1/8000时间内准确地被采集。在这种情况下,1/8000的数字被称为采样间隔(以秒为单位),而采样率只是该间隔的乘法倒数。

采样率类似于视频的帧率或FPS(每秒帧数)测量。视频只是一系列的图片,在这里通常称为 “帧”,非常快速地背对背显示,给人以连续不间断运动或移动的错觉(至少对我们人类来说)。

虽然音频采样率和视频帧率是相似的,但在每一个中保证可用性的通常的最低数字是非常不同的。对于视频来说,为了保证运动的准确描述,每秒至少需要24帧;少于这个数字,运动可能会显得不流畅,连续不间断运动的错觉也无法保持。这一点在帧与帧之间发生的运动越多时尤其适用。此外,每秒1或2帧的视频可能会有 “瞬间 “事件,保证在帧之间被错过。

对于音频来说,要明确地表示英语语音,每秒的最小采样数是8000赫兹。由于各种原因,使用低于这个数字的采样率会导致语音无法被理解,其中一个原因是相似的话语将无法相互区分。较低的采样率会混淆音素或语言中的声音,这些声音具有显着的高频能量;例如,在5000赫兹下,很难将/s/与/sh/或/f/区分开来。

既然我们提到了视频帧,另一个值得详细说明的术语是音频帧。虽然音频样本和音频帧都是以赫兹为单位,但它们并不是一回事。一个音频帧是来自一个或多个音频通道的一个时间实例的音频样本组。

最常见的采样率值是前面提到的8kHz(最常见于电话通信)、44.1kHz(最常见于音乐CD)和48kHz(最常见于电影的音轨)。较低的采样率意味着每秒钟的采样数较少,这反过来又意味着较少的音频数据,因为有较少的采样点来表示音频的数量。采样率的选择取决于需要采集哪些声学伪影。一些声学人工制品如语音语调需要的采样率比声学人工制品如音乐CD中的音乐曲调要低。值得注意的是,更高的采样率需要更多的存储空间和处理能力来处理,尽管这在过去数字存储和处理能力是首要考虑的情况下,现在可能不是那么大的问题。

(2) 采样深度/采样精度/采样大小

除了采样率,也就是我们有多少个音频的数据点,还有采样深度。以每个样本的比特为单位,样本深度(也称为样本精度或样本大小)是音频文件或音频流的第二个重要属性,它代表了每个样本的细节水平,或 “质量”。正如我们上面提到的,每个音频样本只是一个数字,虽然有很多数字有助于表示音频,但你也需要每个单独数字的范围或 “质量 “足够大,以准确表示每个样本或数据点。“质量 “是什么意思?对于一个音频样本来说,它只是意味着该音频样本可以代表更高的振幅范围。8比特的采样深度意味着我们有2^8=256个不同的振幅,而16比特的采样深度意味着我们有2^16=65,536个不同的振幅,以此类推,采样深度更高。电话音频最常见的采样深度是16比特和32比特。在数字录音中,有越多不同的振幅,数字录音听起来就越接近原声事件。

同样,这也类似于我们可能听到的关于图像质量的8位或16位数字。对于图像或视频,图像或视频帧中的每个像素也有一定数量的比特来表示颜色。像素中的比特深度越高,产生的像素颜色就越准确,因为像素有更多的比特来 “描述 “屏幕上要表现的颜色,而且像素或图像总体上看起来更符合人们在现实生活中的样子。从技术上讲,一个像素的比特深度表明该像素可以代表多少种不同的颜色。如果你允许R、G和B中的每一个用8位数字表示,那么每个像素就用3 x 8 = 24位表示。这意味着有2^24~1700万种不同的颜色可以由该像素表示。

(3) 比特率 =(每秒的样本数)x(每个样本的比特数)

将采样率和采样深度联系在一起的是比特率,它是两者的简单乘积。由于采样率是以每秒的样本数来衡量的,而采样深度是以每个样本的比特数来衡量的,因此它是以(每秒的样本数)x(每个样本的比特数)=每秒比特数来衡量的,缩写为bps或kbps。值得注意的是,由于采样深度和比特率是相关的,它们经常被交换使用,但也是错误的。

音频中的比特率因应用而异。要求高音频质量的应用,如音乐,通常有一个更高的比特率,产生更高的质量,或 “更清晰 “的音频。电话音频,包括呼叫中心的音频,不需要高比特率,因此普通电话的比特率通常比音乐CD的比特率低得多。无论是采样率还是比特率,较低的数值可能听起来更糟糕,但同样,根据应用,较低的数值可以节省存储空间和/或处理能力。

总而言之,当涉及到音频时,压缩到底意味着什么?压缩的音频格式,如AAC或MP3,其比特率比采样率和采样深度的真正乘积小一些。这些格式是通过 “外科手术 “从比特流中去除信息来实现的,这意味着在动态情况下那些由于生物原因人耳听不到的频率或振幅不会被存储,从而导致整体文件大小变小。

ffmpeg音频处理-截取、查看、修改采样率

截取音频:

ffmpeg -i input.wav -ss 00:00:05 -t 00:00:10 output.wav

-ss为开始时间 -t为持续时间

查看音频格式:

ffprobe input.wav

修改音频文件采样率:

ffmpeg -i input.wav -ar 16000 output.wav

多通道变单通道

ffmpeg -i input.wav -ac 1 output.wav

转换格式

ffmpeg -i input.mp3 outpit.wav

提取一个通道并重采样

ffmpeg -i input.wav -ac 1 -ar 16000 output.wav

修改采样精度(位数)

ffmpeg -y -i input.wav -acodec pcm_f32le -ac 1 -ar 16000 -vn output.wav

转换音频格式

ffmpeg -i input.flac output.wav

EnCodec: High Fidelity Neural Audio Compression

GitHub:https://github.com/facebookresearch/encodec

Paper:https://arxiv.org/abs/2210.13438

高保真神经网络音频编码器:

本文介绍了meta推出的音频AI Codec,其整体风格深受Google的SoundStream的影响。在其影响下改进了原有的鉴别器,引入语言模型进一步降低码率,并提出了一种提升稳定性的训练策略。

  • 与之前的AI Codec的动机相同,本文同样希望借助深度学习设计一款端到端多码率立体声音频编码器,实现对语音和音乐的低码率压缩并高质量还原。
  • 神经网络天然的抽象特征提取能力使其具有相比传统编码器更强的信号表征压缩能力,低码率的问题相对并不困难;
  • 难点主要有两点:1. 音频的动态范围过大;2. 模型效率问题(计算复杂度和参数量)

本文贡献:

  • 为解决音频动态范围过大的问题,使用庞大多样的训练集以及用鉴别器作为感知损失(这点似乎相比SoundStream)也并未见有什么突破;
  • 限制在单核CPU上实时运行,并采用残差矢量量化(Residual Vector Quantization, RVQ)提高编码效率;
  • 提出了语言模型进一步降低码率;
  • 鉴别器采用多分辨率复数谱STFT鉴别器;
  • 提出了一种balancer以保证GAN训练的稳定性

模型采用的基于GAN的模型,生成器采用时域编码器-量化器-解码器结构,鉴别器采用多分辨率的STFT鉴别器。

编解码器:编解码器采用SEANET,编码器由一层一维卷积对时域波形进行特征提取后经过B个用于降采样的残差单元(即convolurion blocks),而后加入了两层LSTM用于序列建模,最后经过一层卷积得到音频的潜在表征。解码器则是编码器的镜像,其中残差单元的卷积被替换为反卷积用于上采样。根据文中采用的下采样因子(通过卷积的stride实现){2,4,5,8},其编码器将音频下采样320倍(2x4x5x8=320),即传输的一帧中压缩了320倍采样点,因此在采样率为24kHz时1s的音频经编码器输出的时间维数为24000/320=75,48kHz时为48000/320=150。通过卷积的Padding和调整Nomalization去设置模型是否流式。

量化器量化器采用残差矢量量化RVQ,关于RVQ的详细介绍参看[1]和[2]。每个码书包含1024个向量(entries),对于采样率为24kHz的音频,最多使用32个码书,即最大码率为32xlog_2(1024)/13.3=24kbps。为了支持多码率,训练过程中码书数量被设置为{2,4,8,16,32},分别对应1.5kbps,3kbps,6kbps,12kbps,24kbps;且每个码率在训练时所使用的鉴别器是不同的。

语言模型和熵编码:此部分可选,使用Transormer语音模型对RVQ得到的索引映射到新隐藏空间的概率分布,对对应概率密度函数的累积分布函数进行Range Coder熵编码,从而进一步降低码率。

鉴别器:短时傅立叶变换(STFT)。采用多分辨率复数STFT鉴别器,而非TTS中常见的多分辨率Mel谱鉴别器,也没加Multiple-period 鉴别器MPD(消融实验显示多分辨率复数STFT鉴别器性能更优,额外引入MPD有少量性能提升,但考虑训练时长舍弃)。每个分辨率的鉴别器有二维卷积组成,结构如图所示(注意:其中正文和图中的卷积核尺寸不一致,3×8 v.s. 3×9)。

鉴别器采用hinge loss训练,为保证生成器和鉴别器训练平衡稳定,鉴别器以2/3的概率更新其参数

生成器的损失函数:包括重构损失、感知损失(实为对抗损失)以及RVQ的commitment loss三部分,重构损失包括时域和频域两部分,时域损失是波形的L1损失,,频域损失是多时间尺度的Mel谱损失,对抗损失采用hinge loss和特征匹配损失。commitment loss用于使VQ选择的向量满足量化后的变量与未量化的变量间最相近,采用欧式距离度量

数据增广策略:多数据源混合;加混响;音量标准化并随机化增益-10~6 dB;无clip

Demo界面:

1、 https://github.com/facebookresearch/encodec

2、https://ai.honu.io/papers/encodec/samples.html

torchaudio

官网:https://pytorch.org/audio/stable/torchaudio.html

Torchaudio is a library for audio and signal processing with PyTorch. It provides I/O, signal and data processing functions, datasets, model implementations and application components.

读取音频:

使用 torchaudio.load 加载音频数据。torchaudio.load 支持类路径对象和类文件对象。返回值是波形(tensor)和采样率(int)的元组。默认情况下,生成的 tensor 对象的类型为 torch.float32,其值在[−1.0,1.0][−1.0,1.0]内标准化。
waveform, sr = torchaudio.load(filepath, frame_offset=0 , num_frames=-1, normalize=True, channels_first=True)
参数:

filepath (str): 原始音频文件路径;
frame_offset (int): 在此之后开始读取,默认为0,以帧为单位;
num_frames (int): 读取的最大帧数。默认是-1,则表示从frame_offset直到末尾。如果给定文件中没有足够的帧,这个函数可能会返回实际剩余的帧数。
normalize (bool): 当为True时,该函数总是返回float32,并且所有的值被归一化到[-1,1]。如果输入文件是wav,且是整形,若为False时,则会输出int类型。需要注意的是,该参数仅对wav类型的文件起作用, 默认是True;
channels_first (bool)—当为True时,返回的Tensor的维度是[channel, time]。否则,维数为[time, channel], 默认是True。
返回:

waveform (torch.Tensor): 如果输入文件是int类型的wav,且normalization为False,则waveform的数据就为int类型的,否则是float32;如果channel_first=True,则waveform.shape=[channel, time]。
sr (int): 采样率
重采样
waveform = torchaudio.transforms.Resample(orig_freq=16000, new_freq=16000)(waveform)
参数:

orig_freq (int, optional): 原始采样率,默认:16000;
new_freq (int, optional): 转换后的采样率,默认:16000;
resampling_method (str, optional) – 重采样方法,默认: ‘sinc_interpolation’;
waveform (torch.Tensor): 输入音频维度可以是[channel,time],也可以是[time, channel];
返回:

waveform (torch.Tensor): 输出音频维度和输入音频相同,但由于重采样了,time的数值会不同;
保存音频
torchaudio.save(filepath, src, sample_rate, channels_first)
参数:

firepath (str or pathlib.Path): 保存路径;
src (torch.Tensor): 音频数据,必须是二维的;(注:需要转到cpu下的tensor)
sample_rate(int): 采样率;
channels_first (bool): If True, 维度必须是[channel, time],否则是[time, channel]。

The NSynth Dataset

A large-scale and high-quality dataset of annotated musical notes.( 一个大规模、高质量的注释音符数据集。)

下载地址:https://magenta.tensorflow.org/datasets/nsynth#files

Motivation

Recent breakthroughs in generative modeling of images have been predicated on the availability of high-quality and large-scale datasebts such as MNIST, CIFAR and ImageNet. We recognized the need for an audio dataset that was as approachable as those in the image domain.

Audio signals found in the wild contain multi-scale dependencies that prove particularly difficult to model, leading many previous efforts at data-driven audio synthesis to focus on more constrained domains such as texture synthesis or training small parametric models.

We encourage the broader community to use NSynth as a benchmark and entry point into audio machine learning. We also view NSynth as a building block for future datasets and envision a high-quality multi-note dataset for tasks like generation and transcription that involve learning complex language-like dependencies.

Description

NSynth is an audio dataset containing 305,979 musical notes, each with a unique pitch, timbre, and envelope. For 1,006 instruments from commercial sample libraries, we generated four second, monophonic 16kHz audio snippets, referred to as notes, by ranging over every pitch of a standard MIDI pian o (21-108) as well as five different velocities (25, 50, 75, 100, 127). The note was held for the first three seconds and allowed to decay for the final second.

Some instruments are not capable of producing all 88 pitches in this range, resulting in an average of 65.4 pitches per instrument. Furthermore, the commercial sample packs occasionally contain duplicate sounds across multiple velocities, leaving an average of 4.75 unique velocities per pitch.

We also annotated each of the notes with three additional pieces of information based on a combination of human evaluation and heuristic algorithms:

  • Source: The method of sound production for the note’s instrument. This can be one of acoustic or electronic for instruments that were recorded from acoustic or electronic instruments, respectively, or synthetic for synthesized instruments. See their frequencies below.
  • Family: The high-level family of which the note’s instrument is a member. Each instrument is a member of exactly one family. See the complete list and their frequencies below.
  • Qualities: Sonic qualities of the note. See the quality descriptions and their co-occurrences below. Each note is annotated with zero or more qualities.

Format

Files

The NSynth dataset can be download in two formats:

The full dataset is split into three sets:

  • Train [tfrecord | json/wav]: A training set with 289,205 examples. Instruments do not overlap with valid or test.
  • Valid [tfrecord | json/wav]: A validation set with 12,678 examples. Instruments do not overlap with train.
  • Test [tfrecord | json/wav]: A test set with 4,096 examples. Instruments do not overlap with train.

Below we detail how the note features are encoded in the Example protocol buffers and JSON files.

Example Features

Each Example contains the following features.

FeatureTypeDescription
noteint64A unique integer identifier for the note.
note_strbytesA unique string identifier for the note in the format <instrument_str>-<pitch>-<velocity>.
instrumentint64A unique, sequential identifier for the instrument the note was synthesized from.
instrument_strbytesA unique string identifier for the instrument this note was synthesized from in the format <instrument_family_str>-<instrument_production_str>-<instrument_name>.
pitchint64The 0-based MIDI pitch in the range [0, 127].
velocityint64The 0-based MIDI velocity in the range [0, 127].
sample_rateint64The samples per second for the audio feature.
audio*[float]A list of audio samples represented as floating point values in the range [-1,1].
qualities[int64]A binary vector representing which sonic qualities are present in this note.
qualities_str[bytes]A list IDs of which qualities are present in this note selected from the sonic qualities list.
instrument_familyint64The index of the instrument family this instrument is a member of.
instrument_family_strbytesThe ID of the instrument family this instrument is a member of.
instrument_sourceint64The index of the sonic source for this instrument.
instrument_source_strbytesThe ID of the sonic source for this instrument.

Note: the “audio” feature is ommited from the JSON-encoded examples since the audio data is stored separately in WAV files keyed by the “note_str”.

PYTHON — 多进程和多线程

进程与线程的概念,以及为什么要有进程线程,其中有什么区别?

1. 基本概念:

进程是对运行时程序的封装,是系统进行资源调度和分配的的基本单位,实现了操作系统的并发

线程是进程的子任务,是CPU调度和分派的基本单位用于保证程序的实时性,实现进程内部的并发;线程是操作系统可识别的最小执行和调度单位。每个线程都独自占用一个虚拟处理器:独自的寄存器组指令计数器和处理器状态。每个线程完成不同的任务,但是共享同一地址空间(也就是同样的动态内存,映射文件,目标代码等等),打开的文件队列和其他内核资源

2. 区别:
  1. 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。线程依赖于进程而存在。
  2. 进程在执行过程中拥有独立的内存单元,而多个线程共享进程的内存。(资源分配给进程,同一进程的所有线程共享该进程的所有资源。同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储)。但是每个线程拥有自己的栈段,栈段又叫运行时段,用来存放所有局部变量和临时变量。)
  3. 进程是资源分配的最小单位,线程是CPU调度的最小单位
  4. 系统开销: 由于在创建或撤消进程时,系统都要为之分配或回收资源,如内存空间、I/o设备等。因此,操作系统所付出的开销将显着地大于在创建或撤消线程时的开销。类似地,在进行进程切换时,涉及到整个当前进程CPU环境的保存以及新被调度运行的进程的CPU环境的设置。而线程切换只须保存和设置少量寄存器的内容,并不涉及存储器管理方面的操作。可见,进程切换的开销也远大于线程切换的开销
  5. 通信:由于同一进程中的多个线程具有相同的地址空间,致使它们之间的同步和通信的实现,也变得比较容易。进程间通信IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。在有的系统中,线程的切换、同步和通信都无须操作系统内核的干预
  6. 进程编程调试简单可靠性高,但是创建销毁开销大;线程正相反,开销小,切换速度快,但是编程调试相对复杂
  7. 进程间不会相互影响 ;线程一个线程挂掉将导致整个进程挂掉
  8. 进程适应于多核、多机分布;线程适用于多核

进程与线程的一个简单解释:

计算机的核心是CPU,它承担了所有的计算任务。它就像一座工厂,时刻在运行。假定工厂的电力有限,一次只能供给一个车间使用。也就是说,一个车间开工的时候,其他车间都必须停工。背后的含义就是,单个CPU一次只能运行一个任务。进程就好比工厂的车间,它代表CPU所能处理的单个任务。任一时刻,CPU总是运行一个进程,其他进程处于非运行状态。

一个车间里,可以有很多工人。他们协同完成一个任务。线程就好比车间里的工人。一个进程可以包括多个线程。

车间的空间是工人们共享的,比如许多房间是每个工人都可以进出的。这象征一个进程的内存空间是共享的,每个线程都可以使用这些共享内存。可是,每间房间的大小不同,有些房间最多只能容纳一个人,比如厕所。里面有人的时候,其他人就不能进去了。这代表一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。

一个防止他人进入的简单方法,就是门口加一把锁。先到的人锁上门,后到的人看到上锁,就在门口排队,等锁打开再进去。这就叫“互斥锁”(Mutual exclusion,缩写 Mutex),防止多个线程同时读写某一块内存区域。

还有些房间,可以同时容纳n个人,比如厨房。也就是说,如果人数大于n,多出来的人只能在外面等着。这好比某些内存区域,只能供给固定数目的线程使用。

这时的解决方法,就是在门口挂n把钥匙。进去的人就取一把钥匙,出来时再把钥匙挂回原处。后到的人发现钥匙架空了,就知道必须在门口排队等着了。这种做法叫做“信号量”(Semaphore),用来保证多个线程不会互相冲突。

不难看出,mutex是semaphore的一种特殊情况(n=1时)。也就是说,完全可以用后者替代前者。但是,因为mutex较为简单,且效率高,所以在必须保证资源独占的情况下,还是采用这种设计。

操作系统的设计,因此可以归结为三点:

(1)以多进程形式,允许多个任务同时运行;

(2)以多线程形式,允许单个任务分成不同的部分运行;

(3)提供协调机制,一方面防止进程之间和线程之间产生冲突,另一方面允许进程之间和线程之间共享资源。

Python中的进程和线程:

python中的的multiprocess和threading模块用于进行多线程和多进程编程。

Python的多进程编程与multiprocess模块

python的多进程编程主要依靠multiprocess模块。我们先对比两段代码,看看多进程编程的优势。我们模拟了一个非常耗时的任务,计算8的20次方,为了使这个任务显得更耗时,我们还让它sleep 2秒。第一段代码是单进程计算(代码如下所示),我们按顺序执行代码,重复计算2次,并打印出总共耗时。

import time
import os

def long_time_task():
    print('当前进程: {}'.format(os.getpid()))
    time.sleep(2)
    print("结果: {}".format(8 ** 20))

if __name__ == "__main__":
    print('当前母进程: {}'.format(os.getpid()))
    start = time.time()
    for i in range(2):
        long_time_task()

    end = time.time()
    print("用时{}秒".format((end-start)))

输出结果如下,总共耗时4秒,至始至终只有一个进程14236。看来电脑计算8的20次方基本不费时。

当前母进程: 14236
当前进程: 14236
结果: 1152921504606846976
当前进程: 14236
结果: 1152921504606846976
用时4.01080060005188秒

第2段代码是多进程计算代码。我们利用multiprocess模块的Process方法创建了两个新的进程p1和p2来进行并行计算。Process方法接收两个参数, 第一个是target,一般指向函数名,第二个时args,需要向函数传递的参数。对于创建的新进程,调用start()方法即可让其开始。我们可以使用os.getpid()打印出当前进程的名字。

from multiprocessing import Process
import os
import time


def long_time_task(i):
    print('子进程: {} - 任务{}'.format(os.getpid(), i))
    time.sleep(2)
    print("结果: {}".format(8 ** 20))


if __name__=='__main__':
    print('当前母进程: {}'.format(os.getpid()))
    start = time.time()
    p1 = Process(target=long_time_task, args=(1,))
    p2 = Process(target=long_time_task, args=(2,))
    print('等待所有子进程完成。')
    p1.start()
    p2.start()
    p1.join()
    p2.join()
    end = time.time()
    print("总共用时{}秒".format((end - start)))

输出结果如下所示,耗时变为2秒,时间减了一半,可见并发执行的时间明显比顺序执行要快很多。你还可以看到尽管我们只创建了两个进程,可实际运行中却包含里1个母进程和2个子进程。之所以我们使用join()方法就是为了让母进程阻塞,等待子进程都完成后才打印出总共耗时,否则输出时间只是母进程执行的时间。

当前母进程: 6920
等待所有子进程完成。
子进程: 17020 - 任务1
子进程: 5904 - 任务2
结果: 1152921504606846976
结果: 1152921504606846976
总共用时2.131091356277466秒

知识点:

  • 新创建的进程与进程的切换都是要耗资源的,所以平时工作中进程数不能开太大。
  • 同时可以运行的进程数一般受制于CPU的核数。
  • 除了使用Process方法,我们还可以使用Pool类创建多进程。

利用multiprocess模块的Pool类创建多进程

很多时候系统都需要创建多个进程以提高CPU的利用率,当数量较少时,可以手动生成一个个Process实例。当进程数量很多时,或许可以利用循环,但是这需要程序员手动管理系统中并发进程的数量,有时会很麻烦。这时进程池Pool就可以发挥其功效了。可以通过传递参数限制并发进程的数量,默认值为CPU的核数。

Pool类可以提供指定数量的进程供用户调用,当有新的请求提交到Pool中时,如果进程池还没有满,就会创建一个新的进程来执行请求。如果池满,请求就会告知先等待,直到池中有进程结束,才会创建新的进程来执行这些请求。

下面介绍一下multiprocessing 模块下的Pool类的几个方法:

1.apply_async

函数原型:apply_async(func[, args=()[, kwds={}[, callback=None]]])

其作用是向进程池提交需要执行的函数及参数, 各个进程采用非阻塞(异步)的调用方式,即每个子进程只管运行自己的,不管其它进程是否已经完成。这是默认方式。

2.map()

函数原型:map(func, iterable[, chunksize=None])

Pool类中的map方法,与内置的map函数用法行为基本一致,它会使进程阻塞直到结果返回。 注意:虽然第二个参数是一个迭代器,但在实际使用中,必须在整个队列都就绪后,程序才会运行子进程。

3.map_async()

函数原型:map_async(func, iterable[, chunksize[, callback]])
与map用法一致,但是它是非阻塞的。其有关事项见apply_async。

4.close()

关闭进程池(pool),使其不在接受新的任务。

5. terminate()

结束工作进程,不在处理未处理的任务。

6.join()

主进程阻塞等待子进程的退出, join方法要在close或terminate之后使用。

下例是一个简单的multiprocessing.Pool类的实例。因为小编我的CPU是4核的,一次最多可以同时运行4个进程,所以我开启了一个容量为4的进程池。4个进程需要计算5次,你可以想象4个进程并行4次计算任务后,还剩一次计算任务(任务4)没有完成,系统会等待4个进程完成后重新安排一个进程来计算。

from multiprocessing import Pool, cpu_count
import os
import time


def long_time_task(i):
    print('子进程: {} - 任务{}'.format(os.getpid(), i))
    time.sleep(2)
    print("结果: {}".format(8 ** 20))


if __name__=='__main__':
    print("CPU内核数:{}".format(cpu_count()))
    print('当前母进程: {}'.format(os.getpid()))
    start = time.time()
    p = Pool(4)
    for i in range(5):
        p.apply_async(long_time_task, args=(i,))
    print('等待所有子进程完成。')
    p.close()
    p.join()
    end = time.time()
    print("总共用时{}秒".format((end - start)))

知识点:

  • 对Pool对象调用join()方法会等待所有子进程执行完毕,调用join()之前必须先调用close()或terminate()方法,让其不再接受新的Process了。

输出结果如下所示,5个任务(每个任务大约耗时2秒)使用多进程并行计算只需4.37秒,, 耗时减少了60%,可见并行计算优势还是很明显的。

CPU内核数:4
当前母进程: 2556
等待所有子进程完成。
子进程: 16480 - 任务0
子进程: 15216 - 任务1
子进程: 15764 - 任务2
子进程: 10176 - 任务3
结果: 1152921504606846976
结果: 1152921504606846976
子进程: 15216 - 任务4
结果: 1152921504606846976
结果: 1152921504606846976
结果: 1152921504606846976
总共用时4.377134561538696秒

相信大家都知道python解释器中存在GIL(全局解释器锁), 它的作用就是保证同一时刻只有一个线程可以执行代码。由于GIL的存在,很多人认为python中的多线程其实并不是真正的多线程,如果想要充分地使用多核CPU的资源,在python中大部分情况需要使用多进程。然而这并意味着python多线程编程没有意义哦,请继续阅读下文。

多进程间的数据共享与通信

通常,进程之间是相互独立的,每个进程都有独立的内存。通过共享内存(nmap模块),进程之间可以共享对象,使多个进程可以访问同一个变量(地址相同,变量名可能不同)。多进程共享资源必然会导致进程间相互竞争,所以应该尽最大可能防止使用共享状态。还有一种方式就是使用队列queue来实现不同进程间的通信或数据共享,这一点和多线程编程类似。

下例这段代码中中创建了2个独立进程,一个负责写(pw), 一个负责读(pr), 实现了共享一个队列queue。

from multiprocessing import Process, Queue
import os, time, random

# 写数据进程执行的代码:
def write(q):
    print('Process to write: {}'.format(os.getpid()))
    for value in ['A', 'B', 'C']:
        print('Put %s to queue...' % value)
        q.put(value)
        time.sleep(random.random())

# 读数据进程执行的代码:
def read(q):
    print('Process to read:{}'.format(os.getpid()))
    while True:
        value = q.get(True)
        print('Get %s from queue.' % value)

if __name__=='__main__':
    # 父进程创建Queue,并传给各个子进程:
    q = Queue()
    pw = Process(target=write, args=(q,))
    pr = Process(target=read, args=(q,))
    # 启动子进程pw,写入:
    pw.start()
    # 启动子进程pr,读取:
    pr.start()
    # 等待pw结束:
    pw.join()
    # pr进程里是死循环,无法等待其结束,只能强行终止:
    pr.terminate()

输出结果如下所示:

Process to write: 3036
Put A to queue...
Process to read:9408
Get A from queue.
Put B to queue...
Get B from queue.
Put C to queue...
Get C from queue.

python中的多线程(伪多线程):

我们知道 Python 之所以灵活和强大,是因为它是一个解释性语言,边解释边执行,实现这种特性的标准实现叫作 CPython。

它分两步来运行 Python 程序:

  • 首先解析源代码文本,并将其编译为字节码(bytecode)[1]
  • 然后采用基于栈的解释器来运行字节码
  • 不断循环这个过程,直到程序结束或者被终止

灵活性有了,但是为了保证程序执行的稳定性,也付出了巨大的代价:

引入了 全局解释器锁 GIL(global interpreter lock)[2]

以保证同一时间只有一个字节码在运行,这样就不会因为没用事先编译,而引发资源争夺和状态混乱的问题了。

看似 “十全十美” ,但,这样做,就意味着多线程执行时,会被 GIL 变为单线程,无法充分利用硬件资源。

戴着镣铐跳舞

难道 Python 里的多线程真的没用吗?

其实也并不是,虽然了因为 GIL,无法实现真正意义上的多线程,但,多线程机制,还是为我们提供了两个重要的特性。

一:多线程写法可以让某些程序更好写

怎么理解呢?

如果要解决一个需要同时维护多种状态的程序,用单线程是实现是很困难的。

比如要检索一个文本文件中的数据,为了提高检索效率,可以将文件分成小段的来处理,最先在那段中找到了,就结束处理过程。

用单线程的话,很难实现同时兼顾多个分段的情况,只能顺序,或者用二分法执行检索任务。

而采用多线程,可以将每个分段交给每个线程,会轮流执行,相当于同时推荐检索任务,处理起来,效率会比顺序查找大大提高。

二:处理阻塞型 I/O 任务效率更高

阻塞型 I/O 的意思是,当系统需要与文件系统(也包括网络和终端显示)交互时,由于文件系统相比于 CPU 的处理速度慢得多,所以程序会被设置为阻塞状态,即,不再被分配计算资源。

直到文件系统的结果返回,才会被激活,将有机会再次被分配计算资源。

也就是说,处于阻塞状态的程序,会一直等着。

那么如果一个程序是需要不断地从文件系统读取数据,处理后在写入,单线程的话就需要等等读取后,才能处理,等待处理完才能写入,于是处理过程就成了一个个的等待。

而用多线程,当一个处理过程被阻塞之后,就会立即被 GIL 切走,将计算资源分配给其他可以执行的过程,从而提示执行效率。

有了这两个特性,就说明 Python 的多线程并非一无是处,如果能根据情况编写好,效率会大大提高,只不过对于计算密集型的任务,多线程特性爱莫能助。

自强不息

了解到 Python 多线程的问题和解决方案,对于钟爱 Python 的我们,何去何从呢?

有句话用在这里很合适:

求人不如求己

哪怕再怎么厉害的工具或者武器,都无法解决所有的问题,而问题之所以能被解决,主要是因为我们的主观能动性。

对情况进行分析判断,选择合适的解决方案,不就是需要我们做的么?

对于 Python 中 多线程的诟病,我们更多的是看到它阳光和美的一面,而对于需要提升速度的地方,采取合适的方式。这里简单总结一下:

  1. I/O 密集型的任务,采用 Python 的多线程完全没用问题,可以大幅度提高执行效率
  2. 对于计算密集型任务,要看数据依赖性是否低,如果低,采用 ProcessPoolExecutor 代替多线程处理,可以充分利用硬件资源
  3. 如果数据依赖性高,可以考虑将关键的地方该用 C 来实现,一方面 C 本身比 Python 更快,另一方面,C 可以之间使用更底层的多线程机制,而完全不用担心受 GIL 的影响
  4. 大部分情况下,对于只能用多线程处理的任务,不用太多考虑,之间利用 Python 的多线程机制就好了,不用考虑太多

Python的多线程编程与threading模块

python 3中的多进程编程主要依靠threading模块。创建新线程与创建新进程的方法非常类似。threading.Thread方法可以接收两个参数, 第一个是target,一般指向函数名,第二个时args,需要向函数传递的参数。对于创建的新线程,调用start()方法即可让其开始。我们还可以使用current_thread().name打印出当前线程的名字。 下例中我们使用多线程技术重构之前的计算代码。

import threading
import time


def long_time_task(i):
    print('当前子线程: {} - 任务{}'.format(threading.current_thread().name, i))
    time.sleep(2)
    print("结果: {}".format(8 ** 20))


if __name__=='__main__':
    start = time.time()
    print('这是主线程:{}'.format(threading.current_thread().name))
    t1 = threading.Thread(target=long_time_task, args=(1,))
    t2 = threading.Thread(target=long_time_task, args=(2,))
    t1.start()
    t2.start()

    end = time.time()
    print("总共用时{}秒".format((end - start)))

下面是输出结果。为什么总耗时居然是0秒? 我们可以明显看到主线程和子线程其实是独立运行的,主线程根本没有等子线程完成,而是自己结束后就打印了消耗时间。主线程结束后,子线程仍在独立运行,这显然不是我们想要的。

这是主线程:MainThread
当前子线程: Thread-1 - 任务1
当前子线程: Thread-2 - 任务2
总共用时0.0017192363739013672秒
结果: 1152921504606846976
结果: 1152921504606846976

如果要实现主线程和子线程的同步,我们必需使用join方法(代码如下所示)。

import threading
import time


def long_time_task(i):
    print('当前子线程: {} 任务{}'.format(threading.current_thread().name, i))
    time.sleep(2)
    print("结果: {}".format(8 ** 20))


if __name__=='__main__':
    start = time.time()
    print('这是主线程:{}'.format(threading.current_thread().name))
    thread_list = []
    for i in range(1, 3):
        t = threading.Thread(target=long_time_task, args=(i, ))
        thread_list.append(t)

    for t in thread_list:
        t.start()

    for t in thread_list:
        t.join()

    end = time.time()
    print("总共用时{}秒".format((end - start)))

修改代码后的输出如下所示。这时你可以看到主线程在等子线程完成后才答应出总消耗时间(2秒),比正常顺序执行代码(4秒)还是节省了不少时间。

这是主线程:MainThread
当前子线程: Thread - 1 任务1
当前子线程: Thread - 2 任务2
结果: 1152921504606846976
结果: 1152921504606846976
总共用时2.0166890621185303秒

当我们设置多线程时,主线程会创建多个子线程,在python中,默认情况下主线程和子线程独立运行互不干涉。如果希望让主线程等待子线程实现线程的同步,我们需要使用join()方法。如果我们希望一个主线程结束时不再执行子线程,我们应该怎么办呢? 我们可以使用t.setDaemon(True),代码如下所示。

import threading
import time


def long_time_task():
    print('当子线程: {}'.format(threading.current_thread().name))
    time.sleep(2)
    print("结果: {}".format(8 ** 20))


if __name__=='__main__':
    start = time.time()
    print('这是主线程:{}'.format(threading.current_thread().name))
    for i in range(5):
        t = threading.Thread(target=long_time_task, args=())
        t.setDaemon(True)
        t.start()

    end = time.time()
    print("总共用时{}秒".format((end - start)))

通过继承Thread类重写run方法创建新进程

除了使用Thread()方法创建新的线程外,我们还可以通过继承Thread类重写run方法创建新的线程,这种方法更灵活。下例中我们自定义的类为MyThread, 随后我们通过该类的实例化创建了2个子线程。

#-*- encoding:utf-8 -*-
import threading
import time


def long_time_task(i):
    time.sleep(2)
    return 8**20


class MyThread(threading.Thread):
    def __init__(self, func, args , name='', ):
        threading.Thread.__init__(self)
        self.func = func
        self.args = args
        self.name = name
        self.result = None

    def run(self):
        print('开始子进程{}'.format(self.name))
        self.result = self.func(self.args[0],)
        print("结果: {}".format(self.result))
        print('结束子进程{}'.format(self.name))


if __name__=='__main__':
    start = time.time()
    threads = []
    for i in range(1, 3):
        t = MyThread(long_time_task, (i,), str(i))
        threads.append(t)

    for t in threads:
        t.start()
    for t in threads:
        t.join()

    end = time.time()
    print("总共用时{}秒".format((end - start)))

输出结果如下所示:

开始子进程1
开始子进程2
结果: 1152921504606846976
结果: 1152921504606846976
结束子进程1
结束子进程2
总共用时2.005445718765259秒

不同线程间的数据共享

一个进程所含的不同线程间共享内存,这就意味着任何一个变量都可以被任何一个线程修改,因此线程之间共享数据最大的危险在于多个线程同时改一个变量,把内容给改乱了。如果不同线程间有共享的变量,其中一个方法就是在修改前给其上一把锁lock,确保一次只有一个线程能修改它。threading.lock()方法可以轻易实现对一个共享变量的锁定,修改完后release供其它线程使用。比如下例中账户余额balance是一个共享变量,使用lock可以使其不被改乱。

# -*- coding: utf-8 -*

import threading


class Account:
    def __init__(self):
        self.balance = 0

    def add(self, lock):
        # 获得锁
        lock.acquire()
        for i in range(0, 100000):
            self.balance += 1
        # 释放锁
        lock.release()

    def delete(self, lock):
        # 获得锁
        lock.acquire()
        for i in range(0, 100000):
            self.balance -= 1
            # 释放锁
        lock.release()


if __name__ == "__main__":
    account = Account()
    lock = threading.Lock()
    # 创建线程
   thread_add = threading.Thread(target=account.add, args=(lock,), name='Add')
    thread_delete = threading.Thread(target=account.delete, args=(lock,), name='Delete')

    # 启动线程
   thread_add.start()
    thread_delete.start()

    # 等待线程结束
   thread_add.join()
    thread_delete.join()

    print('The final balance is: {}'.format(account.balance))

另一种实现不同线程间数据共享的方法就是使用消息队列queue。不像列表,queue是线程安全的,可以放心使用,见下文。

使用queue队列通信-经典的生产者和消费者模型

下例中创建了两个线程,一个负责生成,一个负责消费,所生成的产品存放在queue里,实现了不同线程间沟通。

from queue import Queue
import random, threading, time


# 生产者类
class Producer(threading.Thread):
    def __init__(self, name, queue):
        threading.Thread.__init__(self, name=name)
        self.queue = queue

    def run(self):
        for i in range(1, 5):
            print("{} is producing {} to the queue!".format(self.getName(), i))
            self.queue.put(i)
            time.sleep(random.randrange(10) / 5)
        print("%s finished!" % self.getName())


# 消费者类
class Consumer(threading.Thread):
    def __init__(self, name, queue):
        threading.Thread.__init__(self, name=name)
        self.queue = queue

    def run(self):
        for i in range(1, 5):
            val = self.queue.get()
            print("{} is consuming {} in the queue.".format(self.getName(), val))
            time.sleep(random.randrange(10))
        print("%s finished!" % self.getName())


def main():
    queue = Queue()
    producer = Producer('Producer', queue)
    consumer = Consumer('Consumer', queue)

    producer.start()
    consumer.start()

    producer.join()
    consumer.join()
    print('All threads finished!')


if __name__ == '__main__':
    main()

队列queue的put方法可以将一个对象obj放入队列中。如果队列已满,此方法将阻塞至队列有空间可用为止。queue的get方法一次返回队列中的一个成员。如果队列为空,此方法将阻塞至队列中有成员可用为止。queue同时还自带emtpy(), full()等方法来判断一个队列是否为空或已满,但是这些方法并不可靠,因为多线程和多进程,在返回结果和使用结果之间,队列中可能添加/删除了成员。

Python多进程和多线程哪个快?

由于GIL的存在,很多人认为Python多进程编程更快,针对多核CPU,理论上来说也是采用多进程更能有效利用资源。网上很多人已做过比较,我直接告诉你结论吧。

  • 对CPU密集型代码(比如循环计算) – 多进程效率更高
  • 对IO密集型代码(比如文件操作,网络爬虫) – 多线程效率更高。

为什么是这样呢?其实也不难理解。对于IO密集型操作,大部分消耗时间其实是等待时间,在等待时间中CPU是不需要工作的,那你在此期间提供双CPU资源也是利用不上的,相反对于CPU密集型代码,2个CPU干活肯定比一个CPU快很多。那么为什么多线程会对IO密集型代码有用呢?这时因为python碰到等待会释放GIL供新的线程使用,实现了线程间的切换。

Vector Quantization 矢量量化 [VQVAE]

http://www.mqasem.net/vectorquantization/vq.html

VQ, 即Vector Quantization,矢量量化,在多个场景下使用,如图像压缩,声音压缩,语音识别等。

Github: https://github.com/lucidrains/vector-quantize-pytorch

矢量量化方法,即Vector Quantization,其具体定义为:将一个向量空间中的点用其中的一个有限子集来进行编码的过程。

什么是VQ?

作为示例,我们在不失一般性的情况下采用二维情况下的向量。 图 1 显示了空间中的一些向量。 与每个向量簇相关联的是一个代表性代码字。 每个代码字都位于其自己的 Voronoi 区域中。 为了说明,这些区域在图 1 中用假想线分隔。 给定一个输入向量,被选择来表示它的代码字是在同一个 Voronoi 区域中的码字。

相互欧几里德距离最近的点代表为码字

欧几里德距离定义为:

VQ如何在压缩中工作?

Vevtor quantizer由两个操作组成。 第一个是编码器,第二个是解码器。 编码器采用输入向量并输出提供最低失真的码字索引。 在这种情况下,通过评估输入向量与码本中每个码字之间的欧几里得距离,可以找到最低失真。 一旦找到最接近的码字,该码字的索引就会通过通道发送(该通道可以是计算机存储、通信通道等)。 当编码器接收到代码字的索引时,它用相关的代码字替换索引。 

在矢量量化编码中,关键是码本的建立和码字搜索算法,如果想对矢量量化有个整体的概览,强烈推荐《Handbook of Image and Video Processing》一书中Fundamentals of Vector Quantization章节。下面对矢量量化中两类典型的方法多阶段矢量量化、乘积量化以及乘积量化的改进做简单介绍。

codebook如何设计?

到目前为止,我们已经讨论了 VQ 的工作方式,但我们还没有讨论如何生成码本。 什么码字最能代表一组给定的输入向量? 应该选多少?

不幸的是,设计一个最能代表输入向量集的密码本是 NP 难的。 这意味着它需要在空间中穷尽搜索最佳可能的码字,并且随着码字数量的增加,搜索呈指数增长(如果你能在多项式时间内找到最佳解决方案,你的名字将永远载入史册)。 因此,我们求助于次优码本设计方案,第一个想到的是最简单的。 它以 Linde-Buzo-Gray 的名字命名为 LBG,Linde-Buzo-Gray 是这个想法的作者。 该算法类似于k-means算法。

算法如下,

  1. 确定码字数 N 或码本的大小。

2. 随机选择N个码字,将其作为初始码本。 可以从一组输入向量中随机选择初始码字。

3. 使用欧几里得距离度量将每个码字周围的向量聚类。 这是通过获取每个输入向量并找到它与每个码字之间的欧几里德距离来完成的。 输入向量属于产生最小距离的码字簇。

4. 计算新的码字集。 这是通过获取每个集群的平均值来完成的。 添加每个向量的分量并除以群集中的向量数。

重复2和3直到所有码字不再变化或者变化很小为止。

该算法是迄今为止最受欢迎的,这是由于它的简单性。 虽然它是局部最优的,但速度很慢。 它慢的原因是因为对于每次迭代,确定每个聚类需要将每个输入向量与码本中的所有码字进行比较。

典型的方法:

下面对矢量量化中两类典型的方法多阶段矢量量化、乘积量化以及乘积量化的改进做简单介绍。

1、多阶段矢量量化:

多阶段矢量量化(Multi-Stage Vector Quantization,MSVQ)也称为残差矢量量化(Residual Vector Quantization, RVQ),它是一种思想,即将编码任务分解为一系列级联过程。级联过程可以用下图直观的展示出来:

如上图所示,对于待量化的向量x,经过一级量化器quantizer1后,得到的量化残差为r1 = x – C1b1,其中C1为一级量化器的码本,b1为x经过一级量化器quantizer1后的表示结果,将一级量化误差r1作为二级量化器的输入,后面过程与此类似。通过这种级联量化的量化方式,当构建的量化器为无穷个时,x可以被这无穷个码本精确表示。上图右侧子图比较直观的描绘了x被多个码本逐步近似的过程。

上述 C1、C2、…、Ci、… 这些码本在构建的时候,可以采用KMeans等方式得到各个量化器的码本。以上面构建的4个级联的码本为例,当得到码本C1、C2、C3、C4后,x量化的结果即可用[b1, b2, b3, b4]表示。对于xq查询向量与x距离的计算,在计算xq与 C1、C2、…、Ci、… 之间的内积距离表后,可以通过查表的方式,获取到非对称距离。

这种多阶段级联的矢量量化方式,相比单阶段一次性量化,极大的降低了码本在训练过程中消耗的计算资源。举个例子,4个阶段的MSVQ,每阶段用KMeans只需构建构建256大小的码本,则对空间分割构建的cell数目为256256256256,效率是很高的,但是如果采用单阶段一次性量化构建4294967296大小的码本,这个码本根本没法用KMeans聚出来。此外在计算距离的时候,采用4阶段的MSVQ方式,只需计算4256次距离的计算构成距离表,然后采用查表方式计算距离,而单阶段一次性量化需要计算4294967296次的距离计算。MSVQ的进一步加速版本是倒排MSVQ,将一级码本视为倒排链,从而构建倒排结构,构建MSVQ倒排结构。

我们可以将MSVQ类比成“深度加深”的过程,下面介绍的非常经典的乘积量化方法,可以为“宽度加宽”的过程。

2、乘积量化:

乘积量化(Product Quantization,PQ)是Herve Jegou在2011年提出的一种非常经典实用的矢量量化索引方法,在工业界向量索引中已得到广泛的引用,并作为主要的向量索引方法,在Fasis有非常高效的实现。乘积量化的核心思想是分段(划分子空间)和聚类,或者说具体应用到ANN近似最近邻搜索上,KMeans是PQ乘积量化子空间数目为1的特例。PQ乘积量化生成码本和量化的过程可以用如下图示来说明:

在训练阶段,针对N个训练样本,假设样本维度为128维,我们将其切分为4个子空间,则每一个子空间的维度为32维,然后我们在每一个子空间中,对子向量采用K-Means对其进行聚类(图中示意聚成256类),这样每一个子空间都能得到一个码本。这样训练样本的每个子段,都可以用子空间的聚类中心来近似,对应的编码即为类中心的ID。如图所示,通过这样一种编码方式,训练样本仅使用的很短的一个编码得以表示,从而达到量化的目的。对于待编码的样本,将它进行相同的切分,然后在各个子空间里逐一找到距离它们最近的类中心,然后用类中心的id来表示它们,即完成了待编码样本的编码。

正如前面所说的,在矢量量化编码中,关键是码本的建立和码字的搜索算法,在上面,我们得到了建立的码本以及量化编码的方式。剩下的重点就是查询样本与dataset中的样本距离如何计算的问题了。

在查询阶段,PQ同样在计算查询样本与dataset中各个样本的距离,只不过这种距离的计算转化为间接近似的方法而获得。PQ乘积量化方法在计算距离的时候,有两种距离计算方式,一种是对称距离,另外一种是非对称距离。非对称距离的损失小(也就是更接近真实距离),实际中也经常采用这种距离计算方式。下面过程示意的是查询样本来到时,以非对称距离的方式(红框标识出来的部分)计算到dataset样本间的计算示意:

具体地,查询向量来到时,按训练样本生成码本的过程,将其同样分成相同的子段,然后在每个子空间中,计算子段到该子空间中所有聚类中心得距离,如图中所示,可以得到4*256个距离,这里为便于后面的理解说明,可以把这些算好的距离称作距离表。在计算库中某个样本到查询向量的距离时,比如编码为(124, 56, 132, 222)这个样本到查询向量的距离时,我们分别到距离表中取各个子段对应的距离即可,比如编码为124这个子段,在第1个算出的256个距离里面把编号为124的那个距离取出来就可,所有子段对应的距离取出来后,将这些子段的距离求和相加,即得到该样本到查询样本间的非对称距离。所有距离算好后,排序后即得到我们最终想要的结果。

从上面这个过程可以很清楚地看出PQ乘积量化能够加速索引的原理:即将全样本的距离计算,转化为到子空间类中心的距离计算。比如上面所举的例子,原本brute-force search的方式计算距离的次数随样本数目N成线性增长,但是经过PQ编码后,对于耗时的距离计算,只要计算4*256次,几乎可以忽略此时间的消耗。另外,从上图也可以看出,对特征进行编码后,可以用一个相对比较短的编码来表示样本,自然对于内存的消耗要大大小于brute-force search的方式。

在某些特殊的场合,我们总是希望获得精确的距离,而不是近似的距离,并且我们总是喜欢获取向量间的余弦相似度(余弦相似度距离范围在[-1,1]之间,便于设置固定的阈值),针对这种场景,可以针对PQ乘积量化得到的前top@K做一个brute-force search的排序。

3、倒排乘积量化

倒排PQ乘积量化(IVFPQ)是PQ乘积量化的更进一步加速版。其加速的本质逃不开在最前面强调的是加速原理:brute-force搜索的方式是在全空间进行搜索,为了加快查找的速度,几乎所有的ANN方法都是通过对全空间分割,将其分割成很多小的子空间,在搜索的时候,通过某种方式,快速锁定在某一(几)子空间,然后在该(几个)子空间里做遍历。在上一小节可以看出,PQ乘积量化计算距离的时候,距离虽然已经预先算好了,但是对于每个样本到查询样本的距离,还是得老老实实挨个去求和相加计算距离。但是,实际上我们感兴趣的是那些跟查询样本相近的样本(姑且称这样的区域为感兴趣区域),也就是说老老实实挨个相加其实做了很多的无用功,如果能够通过某种手段快速将全局遍历锁定为感兴趣区域,则可以舍去不必要的全局计算以及排序。倒排PQ乘积量化的”倒排“,正是这样一种思想的体现,在具体实施手段上,采用的是通过聚类的方式实现感兴趣区域的快速定位,在倒排PQ乘积量化中,聚类可以说应用得淋漓尽致。

倒排PQ乘积量化整个过程如下图所示:

在PQ乘积量化之前,增加了一个粗量化过程。具体地,先对N个训练样本采用KMeans进行聚类,这里聚类的数目一般设置得不应过大,一般设置为1024差不多,这种可以以比较快的速度完成聚类过程。得到了聚类中心后,针对每一个样本x_i,找到其距离最近的类中心c_i后,两者相减得到样本x_i的残差向量(x_i-c_i),后面剩下的过程,就是针对(x_i-c_i)的PQ乘积量化过程,此过程不再赘述。

在查询的时候,通过相同的粗量化,可以快速定位到查询向量属于哪个c_i(即在哪一个感兴趣区域),然后在该感兴趣区域按上面所述的PQ乘积量化距离计算方式计算距离。

4、最优乘积量化

最优乘积量化(Optimal Product Quantization, OPQ)是PQ的一种改进版本。其改进体现在,致力于在子空间分割时,对各子空间的方差进行均衡。在具体实现的时候,我们可以将Optimal的过程实现为一个组件。

通常,用于检索的原始特征维度较高,所以实际在使用PQ等方法构建索引的时候,常会对高维的特征使用PCA等降维方法对特征先做降维处理,这样降维预处理,可以达到两个目的:一是降低特征维度;二是在对向量进行子段切分的时候要求特征各个维度是不相关的,做完PCA之后,可以一定程度缓解这个问题。但是这么做了后,在切分子段的时候,采用顺序切分子段仍然存在一定的问题,这个问题可以借用ITQ中的一个二维平面的例子加以说明:

如上面a图所示,对于PCA降维后的二维空间,假设在做PQ的时候,将子段数目设置为2段,即切分成x和y两个子向量,然后分别在x和y上做聚类(假设聚类中心设置为2)。对a图和c图聚类的结果进行比较,可以明显的发现,a图在y方向上聚类的效果明显差于c图,而PQ又是采用聚类中心来近似原始向量(这里指降维后的向量),也就是c图是我们需要的结果。这个问题可以转化为数据方差来描述:在做PQ编码时,对于切分的各个子空间,我们应尽可能使得各个子空间的方差比较接近,最理想的情况是各个子空间的方差都相等。上图a图中,x和y各个方向的方差明显是差得比较大的,而对于c图,x和y方向各个方向的方差差不多是比较接近的。

为了在切分子段的时候,使得各个子空间的方差尽可能的一致,Herve Jegou在Aggregating local descriptors into a compact image representation中提出使用一个正交矩阵来对PCA降维后的数据再做一次变换,使得各个子空间的方差尽可能的一致。其对应的待优化目标函数见论文的第5页,由于优化该目标函数极其困难,Herve Jegou使用了Householder矩阵来得到该正交矩阵,但是得到的该正交矩阵并不能很好的均衡子空间的方差。

OPQ致力于解决的问题正是对各个子空间方差的均衡。具体到方法上,OPQ借鉴了ITQ的思想,在聚类的时候对聚类中心寻找对应的最优旋转矩阵,使得所有子空间中各个数据点到对应子空间的类中心的L2损失的求和最小。OPQ在具体求解的时候,分为非参求解方法和带参求解方法,具体为:

  • 非参求解方法。跟ITQ的求解过程一样。
  • 带参求解方法。带参求解方法假设数据服从高斯分布,在此条件下,最终可以将求解过程简化为数据经过PCA分解后,特征值如何分组的问题。在实际中,该解法更具备高实用性。

从上面可以看到,倒排乘积量化IVFPQ可以视为1阶段的MSVQ和PQ的结合版本,而OPQ是PQ对子空间方差均衡的改进。基于这样一种普适性的视角,可以构建一种矢量量化框架,MSVQ、PQ、OPQ中的O,都是该矢量量化框架中的基础组件,通过这些组件的组合,我们可以敏捷的得到上面介绍方法的各种实现。

AudioLM

A Language Modeling Approach to Audio Generation

Paper:https://google-research.github.io/seanet/audiolm/examples/

Github: https://github.com/lucidrains/audiolm-pytorch

谷歌开发音频生成模型,创造似真实声音的AI语音。近日,谷歌又开发出一种音频生成 AI。此名为 AudioLM 的模型只通过收听音频即可生成逼真的语音和音乐。

AI 生成的音频其实很常见,像生活中用到的语音助手使用自然语言处理声音。OpenAI 曾开发名为 Jukebox 的 AI 音乐系统也令人印象深刻。但过去用 AI 生成音频,大都需要人们提前准备转录和标记基于文本的训练数据,这需要耗费极大时间和人力。而谷歌在其官方博文中表示:“AudioLM 是纯音频语言模型,无须借助文本来训练,只是从原始音频中进行学习。”

相较之前的类似系统,AudioLM 生成的音频在语音语法、音乐旋律等方面,具有长时间的一致性和高保真度。9 月 7 日,相关论文以《AudioLM: 一种实现音频生成的语言建模方法》(AudioLM: a Language Modeling Approach to Audio Generation)为题提交在 arXiv 上。正如音乐从单个音符构建复杂的音乐短语一样。生成逼真的音频需要以不同比例表示的建模信息。而在所有这些音阶上创建结构良好且连贯的音频序列是一项挑战。据了解,音频语言模型 AudioLM 的背后利用了文本到图像模型的进步来生成音频。

近年来,在大量文本上训练的语言模型,除了对话、总结等文本任务,也在高质量图像上展示出优秀的才能,这体现了语言模型对多类型信号进行建模的能力。但从文本语言模型转向音频语言模型,仍有一些问题需要解决。比如,文本和音频之间不是一一对应关系。同一句话可以有不同风格的呈现方式。此外,谷歌还在其官网提到:“音频的数据速率要更高,用数十个字符就可表示的书面句子,其音频波形通常含有几十万个值。”

为解决这些问题,研究人员采用了语义和声学两种音频令牌。语义令牌(语义标记来自音频框架 w2v-BERT)捕获语音、旋律等局部依赖性和语法、和声等全局长期结构。但是,语义令牌创建的音频保真度较差。因此谷歌还利用了由 SoundStream 神经编解码器生成的声学令牌,该令牌捕获音频波形的详细信息。
在经过对音频序列的声学属性、结构等分别进行建模,以及用精细声学模型为语音添加生动特征几个步骤后,声学令牌被送到 SoundStream 解码器以再建波形。
谷歌还展示 AudioLM 的一般适用性,在被要求继续语音或音乐,并生成在训练期间未看到的新内容时,AudioLM 实现了效果流畅、风格接近的音频生成。特别是,使用 AudioLM 生成的钢琴音乐比使用现有 AI 技术生成的钢琴音乐听起来更自然,后者感觉往往很混乱。

为了生成逼真的钢琴音乐,AudioLM 必须在钢琴键被击中时捕捉每个音符中包含的许多微妙的振动,生成的音乐还必须在一段时间内保持其节奏与和声。对此,在卡内基梅隆大学研究计算机生成音乐的教授罗杰·丹嫩伯格(Roger Dannenberg)对媒体提到,AudioLM 在重新创造人类音乐中固有的一些重复模式方面出奇地擅长,或表明它正在学习某种结构的多个层次。

AudioLM 经过训练,可以了解哪些类型的声音片段经常一起出现,并且反向使用该过程来生成句子。除了音乐,它还可以模仿原始说话者的口音和节奏,并能学习口语中固有的停顿和感叹等特点。经测试,AudioLM 生成的语音与真实语音几乎无法区分。

据了解,AudioLM 远远超出了语音的范围,可以模拟任意音频信号。这可方便扩展到其他类型的音频,以及将 AudioLM 集成到编码器-解码器框架中,以执行文本到语音转换或语音到语音转换等条件任务。然后,更自然的语音生成技术,可以用作视频和幻灯片的背景音轨,帮助改善在医疗等环境下工作的可访问性工具和机器人。

未来,研究团队还希望创造更复杂的声音,就像一个乐队使用不同的乐器,或模仿热带雨林中嘈杂的声音。

Boosting Monocular Depth Estimation-不同尺度的深度图融合的Refine网络

目标: 实现具有一致整体结构的高频细节的深度图结果

GitHub地址:

1、https://github.com/compphoto/BoostingMonocularDepth(main repo)

2、https://github.com/compphoto/BoostYourOwnDepth(Merging Operator:基于深度图的Refine过程的实现)a stand-alone implementation of our merging operator

论文:Boosting Monocular Depth Estimation Models to High-Resolution via
Content-Adaptive Multi-Resolution Merging

卷积神经网络显示出从单个图像估计深度的非凡能力。 然而,由于网络结构和硬件限制,估计的深度图分辨率较低,仅显示整体场景结构,缺乏精细细节,这限制了其适用性。 我们证明了场景结构的一致性与有关输入内容和分辨率的高频细节之间存在权衡。 基于这种二元性,我们提出了一个双重估计框架来改进整个图像的深度估计和一个块选择步骤来添加更多的局部细节。 我们的方法通过基于图像内容合并不同分辨率的估计,获得具有清晰细节的数百万像素深度估计。 我们方法的一个关键优势是我们可以使用任何现成的基于 CNN 的预训练单眼深度估计模型,而无需进一步微调。

(融合网络)该方法是基于优化一个预训练网络的性能,通过合并不同分辨率和不同补丁的估计来生成一个高分辨率的估计。

  • 首先图像以低分辨率和高分辨率的格式输入网络(MiDas网络+10层U-net的Pix2Pix架构作为生成器)
  • 然后,合并MiDaS 的结果(不同高低分辨率估计融合),获得具有一致结构和良好边界定位的基础估计(b)。【高分辨率:细节,低分辨率:结构一致性】
  • 接着,确定图像中的不同补丁,如图(c)显示了选定补丁的子集及其深度估计。
  • 最后,将补丁估计合并到来自(b)的基本估计上,以获得最终的高分辨率结果(d)。
  • 解决的问题: 如何实现在高分辨和高细节度折中的单张图像深度估计

单张图像深度估计的目的是从单张图像中提取场景的结构。与深度相机或者多视角数据所提取的深度信息不同,单张图像深度估计主要依赖于高层单眼深度线索。基于深度学习的方法已然成为单张图像深度估计的标准解决方案。但是对于高分辨深度估计,使其具有好的边界准确性和一致的场景结构,这仍然存在难题。虽然有基于全卷积层结构的方法可以控制任意输入尺寸,但现实中GPU内存、高分辨率数据缺失、CNN感受野尺寸,这些都限制了对应方法的发展。

因此,本文提出采用预训练的单张图像深度估计模型,实现具有高边界准确性的高分辨率结果。

论文的贡献:

  1. 提出一种双重深度估计模型,提高单目图像深度估计的性能;一种基于块选择的方法能加入局部信息到最后的估计结果。
  2. 所提出的方法可以改进最新的单目图像深度估计方法,在提高分辨率与细节的同时,几乎不额外引入计算量。

想法:

我们的主要见解来自观察到单目深度估计网络的输出特性随输入图像的分辨率而变化。 在接近训练分辨率的低分辨率下估计具有一致的结构,但缺乏高频细节。 当以更高分辨率将相同的图像馈送到网络时,可以更好地捕获高频细节,同时估计的结构一致性逐渐降低。(因此:高低分辨率深度图各有优势)

我们的第二个观察是关于输出特征与输入中高级深度线索的数量和分布之间的关系。 网络感受野尺寸主要依赖于网络结构以及训练分辨率。由于单目深度估计依赖于上下文线索,当图像中的这些线索比感受野更远时,网络无法在未接收到足够信息的像素周围生成连贯的深度估计。(选取Patch进行进一步深度图refine)

初步方法

我们的目标是对要合并的单个图像生成多个深度估计,以实现具有一致整体结构的高频细节的结果。这需要:

(i) 检索图像中上下文线索的分布,我们将使用它来确定网络的输入,以及 (ii) 合并操作以将高频细节从一个估计转移到另一个具有结构一致性的估计。
1) 估计的上下文线索。我们使用通过对 RGB 梯度进行阈值处理获得的图像的近似边缘图作为代理。
2)合并单目深度估计。包括输入小分辨率到网络所对应的低分辨率图,相同图像的高分辨率深度图。
使用具有10层U-net的Pix2Pix结构作为生成器。我们训练网络将细粒度的细节从高分辨率输入传输到低分辨率输入
如下图,尽管增加的分辨率提供了更清晰的结果,但在 (c) 之外,深度估计在整体结构方面变得不稳定,通过背景中不正确的工作台深度范围和轮胎周围不切实际的深度梯度可见。

我们展示了在不同分辨率下获得的深度估计,(a)在 MiDaS [34] 的训练分辨率384 × 384下 ,(b)在所选分辨率下,边缘最多间隔 384 个像素,(c)在更高的分辨率下 使 20% 的像素没有附近边缘。(d) 我们的合并网络能够将 (c) 中的细粒度细节融合到 (a) 中的一致结构中,以实现两全其美。

双重估计

我们将二进制膨胀应用于具有不同分辨率的感受野大小内核的边缘图。然后,扩张边缘图停止产生全为一结果的分辨率是每个像素将在前向传递中接收上下文信息的最大分辨率,将其记为 R0 ,分辨率高于R0 的估计将失去结构一致性,但它们在结果中将具有更丰富的高频内容。
因此,我们提出双重估计:为了获得两全其美的结果,我们以两种不同的分辨率将图像馈送到网络,并合并估计以获得具有高频细节的一致结果。如图6所示。

局部提升的块估计

我们提出了一种块选择方法,为图像中的不同区域生成不同分辨率的深度估计,这些区域合并在一起以获得一致的完整结果。
理想情况下,块选择过程应由高级信息指导,以确定用于估计的最佳局部分辨率。但缺乏这样的数据集。因此本文提出一种简单的块选择方法,即做出谨慎的设计决策,以达到可靠的高分辨率深度估计管道,而无需额外的数据集或训练。
1)块选择。通过以基本分辨率平铺图像开始块选择过程,平铺大小等于感受野大小和 1/3 重叠。
2)块估计。采用双重估计方法进行估计。
3)基础分辨率调整。通过在块选择之前将基本深度估计上采样到更高的分辨率来解决这种情况下的这个问题。

  • 实验结果

测试方法:改进两个最新方法MiDaS、 SGR

测试数据:Middleburry 2014 [3]、IBMS-1 [4].

度量标准: 视差空间中的均方根误差 (RMSE)、1.25的像素百分比、方向错误(ORD、序数关系误差的变化

  1. 对比实验

参考

  1. ^Rene Ranftl, Katrin Lasinger, David Hafner, Konrad ´ Schindler, and Vladlen Koltun. Towards robust monocular depth estimation: Mixing datasets for zero-shot cross-dataset transfer. IEEE Trans. Pattern Anal. Mach. Intell., 2020.
  2. ^abcKe Xian, Jianming Zhang, Oliver Wang, Long Mai, Zhe Lin, and Zhiguo Cao. Structure-guided ranking loss for single image depth prediction. In Proc. CVPR, 2020
  3. ^D. Scharstein, H. Hirschmuller, York Kitajima, Greg Krath- ¨ wohl, Nera Nesic, X. Wang, and P. Westling. High-resolution stereo datasets with subpixel-accurate ground truth. In Proc. GCPR, 2014.
  4. ^Tobias Koch, Lukas Liebel, Friedrich Fraundorfer, and Marco Korner. Evaluation of CNN-Based Single-Image ¨ Depth Estimation Methods. In Proc. ECCV Workshops, 2018.

3D Photography –新视点合成

[CVPR 2020] 3D Photography using Context-aware Layered Depth Inpainting

Code地址: https://github.com/vt-vl-lab/3d-photo-inpainting

项目主页: https://shihmengli.github.io/3D-Photo-Inpainting/

本文作者来自弗吉尼亚理工学院、国立清华大学和 Facebook,作者提出了一种从 单张 RGB-D 图像生成 3D 照相 的方法,效果炫酷、惊艳,目前代码已开源。

3D相片的生成主要基于重建和渲染技术,传统的方法需要基线较长的精密的多视角图片捕捉设备或者其他特殊装置,如 Lytro Immerge 和 Facebook Manifold camera。最近有更多的工作尝试从智能相机来生成3D相片,如 Facebook 3D Photos 只需用双摄智能手机拍摄图片,生成RGB-D图像 (彩色图+深度图) 来制作3D相片。

本文同样考虑如何从输入的 RGB-D 图像来合成新的视角以生成3D照片。文章方法对深度图的质量要求并不高,只需要深度不连续处在彩色图和深度图中是合理对齐的即可。深度图可以从 双摄相机通过立体视觉的方式计算 得到,也可以借助 深度学习的方法从单张图片估计 得到,因此应用到智能手机完全没有问题,作者也对这两种来源的深度图进行了测试。

!!!重要:“ 对深度图的质量要求并不高,只需要深度不连续处在彩色图和深度图中是合理对齐的即可

Method

Layered Depth Image

文章方法输入一张 RGB-D 图像,输出分层的深度图像 (Layered Depth Image, LDI),在原始图像中被遮挡的部位填补了颜色和深度。

LDI 类似普通的图像,区别在于每个像素点可以容纳零个或多个像素值,每个 LDI 像素存储一个颜色和一个深度值。与原始论文介绍的 LDI 不同,本文作者显式地表示了像素的局部连通性:每个像素在左右上下四个方向上都存储了零个或最多一个直接相邻的像素指针。LDI 像素与普通图像一样在光滑区域内是四连通的,但是在深度不连续处没有邻接像素。

LDI 是一种对3D相片非常有用的表达,主要在三个方面

  1. 其可以处理任意数量的深度层,可以根据需要适应深度复杂的情况;
  2. 其表达是稀疏的,具有更高的内存和存储效率;
  3. 其可以转换为轻量级的纹理网格表示,直接用于快速渲染。

Method Overview

给定输入的 RGB-D 图像,首先初始化一个单层的四连通的简单 LDI。然后进入预处理阶段,检测深度不连续像素点,并将其分组成简单的相连的深度边。文章算法反复选择深度边来进行修复,先断开边缘上的 LDI 像素,仅考虑边缘处的背景像素进行修复,从边缘的 “已知” 侧提取局部语境区域 (context region),并在 “未知” 侧生成一个合成区域 (synthesis region),合成的区域是一个包含新像素的连续2D区域。作者使用基于学习的方法根据给定的上下文生成其颜色和深度值。修复完成后再将合成的像素合并回 LDI。整个方法以这种方式反复进行,直到所有的深度边缘都经过处理。

Image Preprocessing

初始化阶段首先将输入的 RGB-D 图的深度通道归一化到 0-1 之间,并对深度图进行双边中值滤波,以使得边缘更加明显,再基于此图片生成初始 LDI。然后再根据给定阈值判断相邻像素的视差,找到深度不连续像素,并经过一些简化、处理得到最终的深度不连续边。

初始化阶段首先将输入的 RGB-D 图的深度通道归一化到 0-1 之间,并对深度图进行双边中值滤波,以使得边缘更加明显,再基于此图片生成初始 LDI。然后再根据给定阈值判断相邻像素的视差,找到深度不连续像素,并经过一些简化、处理得到最终的深度不连续边。

Context and Synthesis Regions

接下来每次选择一条深度边借助填补算法来修复背景,首先在深度不连续处断开 LDI 像素连接,得到 (前景、背景) 轮廓像素,然后生成一个合成区域,使用洪水漫淹算法初始化颜色和深度值,再使用深度学习的方法填补该合成区域。

Context-aware Color and Depth Inpainting

给定语境区域和合成区域,这里的目标是合成颜色值和深度值。作者的网络与 EdgeConnect方法类似,将整个修复任务分解成三个子网络:

  • 边修复网络 (edge inpainting network)
  • 颜色修复网络 (color inpainting network)
  • 深度修复网络 (depth inpainting network)
Color &amp;amp;amp;amp; Depth Inpainting

首先将语境区域的边作为输入,使用边修复网络预测合成区域中的深度边,先预测边信息能够推断 (基于边的) 结构 信息,有助于约束 (颜色和深度的) 内容 预测。然后使用修复的边和语境区域的颜色作为输入,使用颜色修复网络预测颜色。最后再使用同样的方法预测深度信息。

下图展示了边指导的深度修复能够准确地延拓深度结构,并能减轻预测的彩色 / 深度不对齐的问题。

Converting to 3D Textured Mesh

通过将所有修复好的颜色和深度值重新集成到原始 LDI 中,形成最终的 3D 纹理网格。使用网格表示可以快速渲染新的视图,而无需对每个视角进行推理,因此文章算法得到的3D表示可以在边缘设备上通过标准图形引擎轻松渲染。

归纳过程:

开始修复之前,LDI上的每个像素都和上下左右的4个像素连通。AI每次选择一条深度边缘去修复,把空间上不相连的像素沿着边缘切开。这样,LDI就分成了前景和背景两个部分(b)。需要修复的部分,就在背景轮廓附近。因为,那里会有些原图上被挡的部分,需要在新视角里露出。前景就不用脑补了。至于怎样修复背景,方法是“联系上下文”,根据周围的像素来推测原本隐藏的部分。所以,首先要把背景分成“合成区”和“上下文区”:合成区是原本被前景挡住的部分,上下文区原本就是背景。

红为合成区,蓝为上下文区

分好两个区,修(nao)复(bu)环节就要开始了。

修复工作由三只小AI负责:先是边缘修复网络,它利用上下文区的边缘,来脑补被挡住的边缘;然后是色彩修复网络和深度修复网络,它们拿到了边缘修复网络提供的物体结构信息,便能更科学地脑补被遮挡的色彩和深度。

边缘修复网络,左为修复前,右为修复后

当三只AI脑补完成,只要把它们给出的结果融进当初的LDI里,新鲜的3D图像便出锅了。然后,把各种不同的视角集合起来,形成动态。

Experimental Results

Visual Comparisons

下图展示了文章方法与其他基于 MPI (Multi-Plane Representation) 方法的对比,文章方法能够合成较为合理的边缘结构,StereoMag 和 PB-MPI 方法在深度不连续处存在缺陷,LLFF 在生成新视角时会有鬼影现象。

参考

  1. 3D Photography using Context-aware Layered Depth Inpainting. Meng-Li Shih, Shih-Yang Su, Johannes Kopf, Jia-Bin Huang. CVPR, 2020.
  2. EdgeConnect: Generative Image Inpainting with Adversarial Edge Learning. Kamyar Nazeri, Eric Ng, Tony Joseph, Faisal Z. Qureshi, Mehran Ebrahimi. ICCV, 2019.

相机内参/外参–坐标转换

摘自:https://zhuanlan.zhihu.com/p/389653208

https://blog.csdn.net/zb1165048017/article/details/71104241

demo演示:http://ksimek.github.io/perspective_camera_toy.html

demo中提供了三种外参接口(世界坐标系,相机坐标系,look-at),三种交互效果不同,前两种的方向相反,世界坐标系中向左移动表示相机坐标系中向右移动,但是它们都有六个参数控制:

tx表示沿着水平方向移动相机
ty表示沿着垂直方向移动相机
tz表示沿着前后方向移动相机
px表示镜头不平移,但是绕x轴做俯仰旋转
py表示镜头不平移,但是绕垂直轴y轴做左右摇头旋转
pz表示镜头不平移,但是绕z轴做顺时针(或者逆时针)旋转
————————————————

demo也提供了内参的接口,包括四个参数控制:

焦距(Focal length):镜头的前后缩进(不是缩放)
轴倾斜(Axis Skew):可以导致球变形,平面上显示椭球形
x0表示主点偏移,相机不动,左右移动成像平面
y0 表示主点偏移,相机不动,上下移动成像平面
————————————————

相机参数都有哪些?估计它们需要的条件?评估所估算的相机参数好坏的标准?

①主要包含内参(intrinsics)、外参(extrinsics)、畸变系数(distortion coefficients)

②估计参数需要3D世界坐标及其对应的2D图像点。比如在重构3D姿态的时候,需要同时输入图片及图片中对应的人的骨骼2D坐标点。

③评估所估计相机参数的方法就是:首先画出相机和校准模式的相对位置;随后计算投影误差;最后计算参数的估算误差。在matlab中有Camera Calibrator来进行相机校准和评估参数精确度。

外参数([R|t])描述世界坐标系与相机坐标系的变换关系,参数包括:旋转R,平移T。

内参数(K)描述相机坐标系,图像坐标系,像素坐标系之间的变换关系,参数包括主点坐标,焦距,单位像素宽与高。

相机成像主要有4个坐标系:

成像的过程实质上是几个坐标系的转换。首先空间中的一点由世界坐标系转换到 摄像机坐标系 ,然后再将其投影到成像平面 ( 图像物理坐标系 ) ,最后再将成像平面上的数据转换到图像平面 (图像像素坐标系 ) 。

对应的左乘矩阵公式

摘要:本文介绍了相机的内参和外参以及推导过程,由三个部分组成:第一部分,相机内参; 第二部分,相机外参;第三部分,总结。

1 相机内参

在左图中,我们把相机看作是针孔,现实世界中的点P经过相机的光心O,投影到物理成像平面上,变为点P’。

在右图中,对这个模型进行了一个简化,将其看作是一个相似三角形。

下面我们来对这个模型进行建模。

设O−x−y−z为相机坐标系,习惯上我们把z轴指向相机前方,x向右,y向下。O为摄像机的光心,也是针孔模型中的针孔。

设真实世界点中的P的坐标为[X,Y,Z]T,成像的点P’的坐标为[X’, Y’, Z’]T, 物理成像平面和光心的距离为f(即为焦距)。

根据右图中的三角形相似关系,有:

其中,有负号是因为坐标轴方向,也就表示了成的像是倒立的。为了表示起来更方便,我们把成像平面从相机的后面对称到前面去,如下图所示。这样,负号就没有了。

在对称后,有:

整理解出P’的坐标:

上面两个式子就描述了P点与它所成像的坐标关系,可以看到,X对应的X’与焦距f有关,与距离Z有关。映射到成像平面上还不够,我们还需要将这个像给放到像素坐标系内。
我们设在物理成像平面上固定着像素平面o-u-v。

设P’在像素平面坐标系上的坐标是[u, v]T

像素坐标系通常定义方式是:原点o’位于图像的左上角,u轴向右与x轴平行,v轴向下与y轴平行。我们设像素坐标在u轴上缩放α倍,在v轴上缩放了β倍。同时,原点平移了[cx, cy]T

因此可以得到P’与像素坐标的关系:

用齐次坐标,把上式写出矩阵的形式:

上式中,K即为相机的内参矩阵(Intrinsics)。通常来说,相机的内参在出厂之后就是固定的了。

2 相机外参

相机外参的作用是把坐标从【世界坐标系】转换到【相机坐标系】中

在上面的推导中,我们用的是P在相机坐标系的坐标(也就是以相机为O点),所以我们应该先将世界坐标系中的Pw给变换到相机坐标系中的P。

相机的位姿由旋转矩阵R和平移向量t来描述,因此:

旋转矩阵:R欧拉角:wiki百科

Rot(x, θ)   表示绕X轴旋转  θ表示旋转的角度  其它同理。矩阵右下角的表示放大倍数,矩阵第4行和第4列可以不要

3 镜头畸变

透镜由于制造精度以及组装工艺的偏差会引入畸变,导致原始图像的失真。镜头的畸变分为径向畸变和切向畸变两类。

  1. 径向畸变

顾名思义,径向畸变就是沿着透镜半径方向分布的畸变,产生原因是光线在原理透镜中心的地方比靠近中心的地方更加弯曲,这种畸变在普通廉价的镜头中表现更加明显,径向畸变主要包括桶形畸变和枕形畸变两种。以下分别是枕形和桶形畸变示意图:

成像仪光轴中心的畸变为0,沿着镜头半径方向向边缘移动,畸变越来越严重。畸变的数学模型可以用主点(principle point)周围的泰勒级数展开式的前几项进行描述,通常使用前两项,即k1和k2,对于畸变很大的镜头,如鱼眼镜头,可以增加使用第三项k3来进行描述,成像仪上某点根据其在径向方向上的分布位置,调节公式为:

公式里(x0,y0)是畸变点在成像仪上的原始位置,(x,y)是畸变较真后新的位置,下图是距离光心不同距离上的点经过透镜径向畸变后点位的偏移示意图,可以看到,距离光心越远,径向位移越大,表示畸变也越大,在光心附近,几乎没有偏移。

  1. 切向畸变
    切向畸变是由于透镜本身与相机传感器平面(成像平面)或图像平面不平行而产生的,这种情况多是由于透镜被粘贴到镜头模组上的安装偏差导致。畸变模型可以用两个额外的参数p1和p2来描述:

下图显示某个透镜的切向畸变示意图,大体上畸变位移相对于左下——右上角的连线对称的,说明该镜头在垂直于该方向上有一个旋转角度。

径向畸变和切向畸变模型中一共有5个畸变参数,在Opencv中他们被排列成一个5*1的矩阵,依次包含k1、k2、p1、p2、k3,经常被定义为Mat矩阵的形式,如Mat distCoeffs=Mat(1,5,CV_32FC1,Scalar::all(0));这5个参数就是相机标定中需要确定的相机的5个畸变系数。求得这5个参数后,就可以校正由于镜头畸变引起的图像的变形失真,下图显示根据镜头畸变系数校正后的效果:

3 总结

本文介绍了:

  1. 从相机坐标系转换到像素坐标系中,相机内参的作用
  2. 从世界坐标系转换到相机坐标系中,相机外参的作用

相机内参是这样的一个矩阵:

里面的参数一般都是相机出厂就定下来的,可以通过相机标定的方式人为计算出来。

相机外参是旋转矩阵R和平移向量t构成,一般来说写成:

这个矩阵决定了相机的位姿。