Pre-emphasis☆超·大·全·补完☆

更新:我感觉我搜了下中文圈谈pre-emphasis的不多,而且很多没提到EAC无法识别subcode的问题,我觉得有责任感(大雾)写个摘要提炼下重点。这样别人找起来方便也不用读我这私货奇多的裹脚布。

TL;DR

  1. 关于Pre-emphasis的介绍见前文,那个不长。但是简单来说,为了减少噪声和量化误差的影响,部分老CD(尤其是日本八十年代)会对CD进行预增益,加强高频,并在播放的时候靠CD Player进行de-emphasis来还原。抓碟时需要手动进行这一过程,否则抓出来的音频高频偏高。
  2. 存在只有部分track有预增益的CD
  3. Pre-em的Flag可以写在CDDA的TOC里,也可以写在subcode(正常应该两者皆有)。抓轨软件识别出之后,一般会在CUE里加上FLAGS PRE
  4. 主流rip软件EAC只能识别TOC里的Pre-em flag,不能识别subcode里的!很重要所以再说一遍,EAC的pre-em识别功能是个残疾!要正确地读取subcode里的pre-em flag,Windows推荐使用CUERipper,Mac推荐XLD。
  5. pre-em的音轨即使识别出来,也需要处理。可以用SOX等软件直接进行de-em转换,也可以用foobar2000配合pre-emphasis=1(需要手动添加,CUE里的不识别)的tag和foo_deemph.dll、foo_dsp_deemph.dll等插件进行回放时即时de-em(不修改文件)。
  6. foo_deemph vs foo_dsp_deemph: 前者为后处理滤镜,只支持无损音频(fb2k本身限制),但是可以参与ReplayGain计算(即:RG scan是在考虑了de-em之后计算),另外BUG:暂不支持无损+内嵌CUE+仅部分轨有pre-emphasis=1的特殊情况。后者为DSP(需要DSP中勾选),支持所有格式但是不参与RG计算,无法计算出准确的音量增益。
  7. 可以利用频谱分析(推荐使用周期图,最清晰)来对比音轨和识别pre-em过的音轨。

下面正文。


我发现一个趋势:每次我写点啥文章,绝对会在写完之后发现更多该topic相关的东西,然后……写出一个比本篇还长的续。这次大概也不例外了。

Pre-emphasis危机

上篇说到Pre-emphasis的事情,有提到我对我之前的收藏里有多少没发现的Pre-emphasis的担忧,这不果然说中了。因为知道了这玩意的存在,所以在抓うしろゆびさされ組的第二专《∞》的时候专门留了个心眼,发现和首专不同,这张抓出来并没有pre-emphasis的tag,听起来也一切正常。不过和手头其他版本对比的时候我想起来当年我就觉得《おニャン子クラブ大全集》(下称:大全集)里的「バナナの涙」不太对,但是当时手头其他版本都是MP3的只有这个是无损,所以以为是别的都有问题。这和原版专辑《∞》里的一比就听出问题了:《大全集》里的版本明显尖锐许多。

我赶紧去看《大全集》的CUE,发现并没有FLAG PRE。这下我彻底恐慌了:连官方出的精选集都能搞这幺蛾子(有pre-emphasis却没标明),这到底要怎么避免哦?我进而把这《大全集》的DISC 4 with おニャン子クラブ集又重头到尾听了一遍(同时不断和手头有的其他版本对比),果然发现有多首,包括うしろゆびさされ組的全部四轨都有pre-em的问题。其他有些虽然听着偏尖锐,但是你别说,这个东西单独听真的很难说清楚到底有没有pre-em,尤其是要知道这个pre-emph主要是对某些音色的乐器影响比较大(比如上次提到过的,架子鼓里的钹),对人声尤其是比较粗的人声其实变化不大(嗯新田恵利这种很尖锐的声线就明显多了w),某些歌曲甚至对比着听都不明显。而且和其他版本对比本身也有个问题——你怎么知道其他版本就是正确的呢?尤其是我手里的其他版本很多连无损都不是。

当然,这张DISC 4本身就是从小猫单飞的各个单曲/专辑里采集来的,音源不一,所以有这种问题也算在我的意料之内,不过接下来的事情就更蛋疼了。《大全集》大部分碟是小猫俱乐部八十年代的原创录音专辑+额外曲目的形式。所以如果不出意外,至少前面和原专辑一样的部分的pre-emphasis状态应该统一:要么都有要么都没有。还是通过对比的方式,我发现至少前三张碟的大部分都OK——并没有pre-emph,还顺便发现了反而是我最早下的一张的小猫的碟(网易云扒的MP3),算是最经典的二专《PANIC THE WORLD》(下称:PANIC),其实有pre-em(汗,这碟我感觉听了有快上百遍了,当时也没觉得有啥问题……现在再去听真是尖到不行)。

用Foobar2000回放有损的pre-em音频的方案

这里插播一个实践上的问题。上次提到de-emphasis要用foobar的两个插件,lvqcl开发的foo_deemph和foo_dsp_deemph。前者是后处理插件,也就是说无设置,会无脑应用到后处理里(包含回放和转换,如果勾了后处理选项)。我之前是用的这个。结果这个的问题在于他仅作用于无损音频——关于这点我问了作者,并不是他不想改,而是foobar对后处理插件的限制(或者说foobar的工作流程如此)。所以,我上面提到那个,有pre-emphasis的MP3专辑《PANIC》,自然只能用DSP版本才行。但是这就又引出一个问题:回放增益(ReplayGain)。在进行了deemph之后,音频改动还是蛮大的,回放增益扫描的结果也不会一样。事实上,一般一个音轨deemph之后,回放增益得加个2dB。之前用foo_deemph的时候倒是OK,因为回放增益在计算时会自动考虑进去所有后处理;但是现在换了DSP版,这个在计算回放增益的时候是不工作的。

解决办法倒不是没有,就是非常啰嗦:

  1. 把MP3文件转换成WAV;
  2. 现在因为是无损了,所以foo_deemph工作了,可以扫描RG了。
  3. 把RG用MP3tag复制RG到之前的MP3(这是一个傻逼点,因为foobar2000刻意不支持文件间复制RG信息,只能手动一个个输入…一般的标签倒是支持)。
  4. 然后回放的时候……你得换成foo_dsp_deemph,因为只有这个支持有损(别忘了pre-emphasis的tag还得有,DSP里得勾上)。虽然顺序变成了先apply RG再apply deemph,从数学原理上来讲两者等效。

另外,因为回放的时候你不能同时装foo_deemph和foo_dsp_deemph(否则无损的有pre-emphasis tag的音频就会被deemph两次了),所以整个流程你得重启foobar N次,动用两个软件(foobar2000,Mp3Tag)。

不过这个流程可以稍微简化一下:在转换MP3到WAV的时候,直接用DSP版的把deemph硬编码进去——别忘了之后删掉pre-emphasis的tag——然后和上面就一样了。这个方案的区别在于完全用不到后处理版的foo_deemph,不需要重启foobar,但是总体而言还是很麻烦。

而且更麻烦的一点是,如果你改用foo_dsp_deemph(凡是你要给有损音频de-em,这是唯一的选择),那么就连原来没问题的无损的音频的RG扫描都不能直接进行了。所以,果然我的第一要务是赶紧把这些垃圾MP3洗版买二手CD洗掉。

哦顺便说句,foo_dsp_deemph还有另外一个同名的,由开发者mudlord从他的那个巨型插件foo_dsp_effect里分离出来的独立组件。之前说的有误,这个插件虽然名字叫DSP,但是其实包含上述两个插件的的功能,即后处理+DSP:在安装该插件之后,自动含对有pre_emphasis =1 tag的文件进行de-em;与此同时,你还可以在DSP开启强制de-em。而且其提供了俩DSP,外一个是黑胶的RIAA Curve

至于de-em算法方面,我对比了lvqcl版、mudlord版以及sox,虽然均非bitwise identical但是基本频谱一致,应该不会有任何听感上的差别。(7/5更新)

「およしになってねTEACHER」之谜

实践方面的问题就插播完毕,回到之前的话题,既然我们知道三专《PANIC》有pre-emphasis,那前两专呢?一专《KICK OFF》我手头的是无损,打开CUE一看发现里面大大的FLAGS PRE,这就好说了,直接给wav文件加个tag完事儿。二专《夢カタログ》(下称:夢)我之前虽然下了个MP3,但一直都没解压,因为反正《大全集》里都有了;这不为了对比我又翻出来了。果然这二专一听,也是有pre-emphasis的,但是前面说到的“蛋疼”的事情来了:我发现在de-emphasis之后,二专的大部分曲目听感和《大全集》版一致,唯有第一轨「およしになってねTEACHER」(即第二单曲),不但两者不同,和3专《PANIC》里收录的版本也不同!我又找了多个版本,最后从“尖锐程度”从大到小排列大概是这么个样子(其实有的差别很小,我用了耳朵听之外的方法检测,不过这是下一章的重点。另外,听的时候因为一定要保持平均响度高度一致(否则怎么对比),由于上面提到过的RG+pre-em的配合问题,这里请暂时配合on the fly版的RG DSP:foo_r128norm使用,并放在foo_dsp_deemph的后面):

  1. 《夢》(pre-emph)
  2. 《大全集》 = Single版
  3. 《夢》(de-emph)
  4. 《PANIC》(de-emph) =《SUPER BEST》
  5. 《大全集》(de-emph) = Single版 (de-emph)

这里,4这两个版本听感完全一致,外加精选集《SUPER BEST》是我从实体碟抓的无损(不过有个暂不相关的问题后面讲),所以姑且称之为“标准版本”。那么《夢》和重制《夢》的《大全集》的问题在于:第一,两者莫名地不一致,虽然差别不大;第二,《夢》即使加了de-em(根据其他音轨推算应该加),也比4这个标准版要尖锐一些(但是不至于刺耳);至于《大全集》版,不加de-em处于《夢》加和不加之间,加了就反而比4这个标准版还要低沉。

如果再进一步从听感上选最合理的然后简化,有三个版本(还是按照高频强度从高到低排列):

  • 《大全集》 = Single版
  • 《夢》(de-emph)
  • 《PANIC》(de-emph) =《SUPER BEST》

几乎可以肯定《大全集》制作的时候,是使用单曲音源替换掉了第一轨——因为手头的单曲版和他的特征一致,而且否则也无法解释为啥其他轨《大全集》和《夢》de-em后听感完全一致,唯有这个不同。但是要注意的是「およしになってねTEACHER」这张单曲是只有黑胶版的,并没有发过CD;我手头这个“单曲版”的MP3到底怎么从黑胶Rip出来,规不规范,无从得知;不过对于制作方波利佳音,他肯定有母带所以倒不是问题。

至于《夢》的版本,在正确地加了de-emph之后其实和《大全集》版/单曲版区别并不大,这里这里我大胆猜测,两者的区别可能仅仅是黑胶和CD制作上客观导致的(毕竟一个analog一个digital)。又或者是《夢》里的版本是Album version(当年album version基本都是没任何标记)。

至于后面为什么《PANIC》以及《SUPER BEST》这两张精选集里收的版本互相一致、却和上面的单曲版和专辑版都不同就很难理解了。而且这个“不同”还是相当明显的——从最最开始的前奏就很明显可以听出。一般而言,这类精选集收的应该都是单曲版,其不同的原因现在也很难考证了。

利用频谱分析

上述「TEACHER」这场风波,让我了解到纯靠听来比较音轨,实在是不靠谱,尤其是差别小的时候。而且很多时候,我手头只有一个版本,也很难判断是否有pre-em。介于pre-em技术的原理就是加个EQ,所以第一时间想到的就是用频谱来分析了。

一般而言,直接FFT后的Amplitude spectrum就OK,用频谱密度(Spectral Density)也行,不过基本趋势都是一样的。具体实现上前者没什么好说的,注意用双侧转换成单侧(参见Matlab的fft说明文档里的范例)就是。谱密度的话,可以直接用PeriodogramMatlab函数),或者用Welch’s methodMatlab函数)可能更美观些。

不过,在分析前,得先做一些前处理。为了方便对比,首先需要注意的是音量均衡的问题。虽然我可以在foobar里跑RG,但是为了简单起见我做了个纯粹根据数学RMS(均方根)来均衡强度的函数:


function [ y ] = normalizerms( x, targetdB )
%NORMALIZERMS Summary of this function goes here
% Detailed explanation goes here
%normalize
targetrms=10^(targetdB/20);
myrms=rms(x);
% valueDBFS = 20*log10(abs(myrms));
y=x/(myrms/targetrms);
end

view raw

normalizerms.m

hosted with ❤ by GitHub

这样,我就不用操心响度不一不便于对比的问题了。实际操作上,音轨通过audioread导入之后,要先downmix到单音道,然后跑这个来均衡强度:

 [wave,Fs]=audioread(file);
 x=(wave(:,1)+wave(:,2))/2; 
 x=normalizerms(x, -20);

这里我选了-20dB这个比较低的目标RMS,因为我不想导致clipping。

然后再把信号跑上面的提到的几种频谱法就行了,下面我主要以Periodogram为例。那么,就让我们先随便找一首歌,来对比pre-em和de-em的区别。用的曲目是上次提过的「偏差値BOY」,无损音源。

(左边:原始文件(pre-em),右边:de-em之后。点击查看幻灯片或者大图)

和想象的一样,两者的周期图的区别较为明显,后者高频方向向下倾斜的趋势更明显,也符合de-em的原理。基本上,如果看到曲线接近后者,可以认为是正常的歌曲;如果是前者这种很平的,那就是有pre-em。嗯,还是有点模糊…不过比纯靠听稍微强点。

接下来就看看上面提到过的「TEACHER」的部分版本。

左起:《SUPER》版,《夢》版(de-em后)和《大全集》版(de-em后)

其中,《夢》版(de-em)在16kHZ左右的狂跌是因为MP3格式所导致。不过除掉这部分不看,也可以看出和1的标准版相比,整体更平(即:高频更高)。而最后一个的《大全集》版(de-em后)则比1更斜(即:高频更低)一些。

这里有一点要注意,就是《大全集》版(de-em后)以及这里没贴出来的单曲版(两者基本一样)都在大约1.5kHZ的地方有一个凹槽,这是很不正常的(别的版本都没有)。同样的凹槽其实我在很多音轨上都见过,我强烈怀疑是早期母带制作时,在压制或者是analog转digital时什么过程的痕迹。也许这也能解释为什么《大全集》版(de-em后)比标准版要低,虽然两者听感几乎一致。

其他几种visualization的方法既然我都做了,就贴一下让大家感受一下。

Welch’s method估计PSD:参数我都瞎搞的,不过基本而言,x后面的第一个参数window越小分割次数越高,平滑度越高,也越慢。

左起:《SUPER》版,《夢》版(de-em后)和《大全集》版(de-em后)

另外,我一直想模拟一下类似foobar频谱的按band分开的柱状图:

QQ图片20170701184057

但是有太多的搞不清楚的问题:第一,band怎么分?Foobar明显不是用线性的,每个band的label分别是(以20个band为例):

[50,69,94,129,176,241,331,453,620,850,1200,1600,2200,3000,4100,5600,7700,11000,14000,20000]

看不出有什么规律,只知道肯定不是线性,手动回归了一下大概是类似y=10^(1.5633+0.1368x) (x=1:20)的样子。

第二,每个band里面到底应该怎么把里面的点“累积”起来?是直接相加(因为是离散的)?

总之,我瞎搞了一番,最后成图如下:

左起:《SUPER》版,《夢》版(de-em后)和《大全集》版(de-em后)。单位写错了应该是HZ,懒得改了。

完整代码(含所有的可视化方式,在上方切换):


filenames = {'samples/teacher/super.wav','samples/teacher/yume_deem.wav','samples/teacher/yume2005_deem.wav'};
%'samples/panic.wav','samples/super.wav', 'samples/yume.wav','samples/yume2005.wav','samples/single.mp3'
%filenames = {'old/good_rg.wav','old/bad_rg.wav'};
method = 'peri'; % peri, welch, fft, fftband
close all
for i = 1:length(filenames)
file = filenames{i};
[wave,Fs]=audioread(file);
x=(wave(:,1)+wave(:,2))/2; %Downmix
% %Filter
% N = 200; % FIR filter order
% Fp = 15000; % 20 kHz passband-edge frequency
% Rp = 0.00057565; % Corresponds to 0.01 dB peak-to-peak ripple
% Rst = 1e-4; % Corresponds to 80 dB stopband attenuation
% eqnum = firceqrip(N,Fp/(Fs/2),[Rp Rst],'passedge'); % eqnum = vec of coeffs
% lowpassFIR = dsp.FIRFilter('Numerator',eqnum);
% x=lowpassFIR(x);
x=normalizerms(x, -20);
switch method
case 'peri' % periodogram
figure
periodogram(x,rectwin(length(x)),length(x),Fs)
xlim([0 20])
ylim([-180 -20])
case 'welch' % Welch's power spectral density estimate
[pxx,f] = pwelch(x,2000,[],[],Fs); % Disclamer: I have zero idea about those parameters
figure
plot(f,10*log10(pxx)) % You can remove dB conversion
xlabel('Frequency (Hz)')
ylabel('Magnitude (dB)')
xlim([0 20000])
ylim([-105 -40])
case 'fft' % FFT (amplitude spectrum)
L=length(x);
f = Fs*(0:(L/2))/L;
Y = fft(x);
P2 = abs(Y/L); % You can choose to not divide it by L, no difference
P1 = P2(1:L/2+1);
P1(2:end-1) = 2*P1(2:end-1);
figure
plot(f,P1) % you can convert it to dB too, make sure to use 'voltage'
xlabel('f (Hz)')
ylabel('|P1(f)|')
ylim([0 0.004])
xlim([0 20000])
case 'fftband'
L=length(x);
f = Fs*(0:(L/2))/L;
Y = fft(x);
P2 = abs(Y); % I don't divide by L here, because I'm going to use dB later
P1 = P2(1:L/2+1);
P1(2:end-1) = 2*P1(2:end-1);
bars = 20;
% % Linear spacing
% fvalues = linspace(0,20000,bars+1);
% fvalues = fvalues(2:end);
% % Log10 spacing
% fvalues = logspace(-1,log10(20000),bars);
% Copied from FB
fvalues=[50,69,94,129,176,241,331,453,620,850,1200,1600,2200,3000,4100,5600,7700,11000,14000,20000];
fband = zeros(length(fvalues),1);
% Again, I don'tknow shoul you just add them together.
for k = 1:length(f)
for j = 1:length(fvalues)
if f(k) < fvalues(j)
fband(j) = fband(j)+P1(k);
break
end
end
end
figure
histogram('BinEdges',0:length(fvalues),'BinCounts',round(db(fband,'voltage'))')
ylim([0 180])
labels = [0 fvalues(2:2:length(fvalues))];
xticklabels(num2cell(labels))
xlabel('f (Hz)')
ylabel('dB')
end
title(file)
end

CD rip:正确读取subcode的pre-em flags

之前说过,我最怕的是厂商把明明有pre-em的曲目压到CD里面却不处理也不标明——因为这样作为听众哪怕有实体碟那么听到的也是错误的。那么我上面提到的几个例子,尤其是我手头有无损的《大全集》真的一定就是这样吗?答案是否定的。没错,虽然我手里的大全集的CUE,并没有FLAGS PRE,但是这并不代表原碟就一定没有对pre-em进行标记。

这要先从pre-em的标记方式说起。在CDDA中,可以有两个地方标记各种flag:一个是在目录(Table of contents,TOC)中,一个是在“subcode”(又称subchannel data)中。其实CDDA实际的结构要更复杂一些,简单地说就是除了16位的音频数据的部分,其他有大量的类似元数据的subcode存在。其中,每一轨都可以有自己的subcode,而在整张光碟的头部、尾部又各有一个区域完全是subcode。其中,头部的subcode包括了一些诸如discid之类的元数据,另外也有TOC(所谓的TRACK 00):TOC其实就是对整张专辑各个轨道的简单描述,主要是每轨的开始时间(便于跳转),但是也可以包括诸如pre-em在内的tag。

然而与此同时,在每一轨的subcode中,同样可以包含这样的信息。这问题就来了:虽然理想情况下两者应该是一致的,但是大量实践证明,有许多CD,只在其中一个中包含了pre-em的flag(一般是subcode)。

这俩不一致本身其实不是什么大事儿,但是问题在于,Win平台下最流行的CDrip软件,大名名鼎鼎的的EAC,不支持subcode flag。呃,其实准确地说,也是有难言之隐:早在01年左右的0.9x版本的EAC,包含一个叫做“Detect TOC Manually”的功能,说白了就是用subcode里的信息手动重建TOC(而不依赖于TRACK 00的TOC),这个功能就能检测到subcode-only的pre-em tag。但是由于当时有些CD利用hack TOC的方式来防复制,所以EAC这种功能有违反欧洲法规的风险(等于你绕过了TOC的加密),作者就在后面移除了该功能。大概这也是为什么我在论坛有时候会看到有人强调要用旧版EAC的缘故?

无论如何,身为几乎在烧友圈(至少国内)被神化的EAC居然缺失这么明显一个功能,也是略显讽刺。所以,即使碰到有的碟,正确用EAC抓取、CUE没有pre-em的flag,听起来却尖锐,也就是上面说的《大全集》中的某些轨,也不一定就是厂商搞砸,而是subcode的flag没被抓出来。当然现实来说,《大全集》是21世纪的2005年发售的,感觉还在发行带pre-em的CD概率并不高……大概还是搞砸了罢,这个没有实体版实在是无从确认了。别忘了之后波利佳音又洗过一版,说不定正是为了擦屁股呢?(笑)

不过虽然那个手头没有实体无法验证,我之所以会深入研究这个正是因为我手里就有一张这样的碟:[1986-10-21] [D32P6003] おニャン子クラブ – スーパーベスト(即前文所说的《SUPER BEST》)。这碟我用EAC抓,是显示没有pre-em:

QQ截图20170701222211

抓出来的CUE自然也没有flag。但是实际上呢?用听的就知道,第一轨「お先に失礼」肯定有pre-em,后面的第二轨又明显没有。可见这碟应该是属于部分pre-em那种(考虑到是选集,也说得通)。

既然EAC不行,我们只能换软件了。hydrogenaudio提供了一个非常完善的ripper软件列表。不过排除一些收费软件和非Win平台的(比如很有名的Mac平台的XLD),外加上我从这贴了解到的其他一些,我大概筛选出这几个:

于是先来试试最简单的CUERipper。

QQ图片20170701234441

选项非常简单,要我说是EAC过分复杂了。虽然这里看不出来,但是抓出来的CUE确实有正确的FLAGS PRE没错。具体来说,是tr. 1/6/9/11有,也和试听听感完全一致。

再让我们试试其他俩软件。cdda2wav和cdrdao都是命令行工具;而且更不方便的是两者官方网站都不提供binary,只有source code。cdda2wav现在是软件包“cdrtools”(原名:cdrecord)的一部分,这里可以找到Win-32原生的编译版本(最新:v3.02a07),或者用第三方的GUI版本——cdrtfe,里面也有编译好的cdda2wav(不过是虚拟版,如果你要单独运行,得把cygwin目录下的cygwin1.dll复制到和cdda2wav.exe一起)。

但是这个cdda2wav……用起来有问题。还是上面这张碟,让我们跑个-J(仅输出信息)来看看:

C:\Users\Administrator\Desktop\schily-cdrtools-3.02a07\win32>cdda2wav -J
No target specified, trying to find one...
Using dev=7,0,0.
Type: ROM, Vendor 'HL-DT-ST' Model 'DVDRAM SP80NB60 ' Revision 'RA00' MMC+CDDA
261632 bytes buffer memory requested, transfer size 64512 bytes, 4 buffers, 27 s
ectors
#Cdda2wav version 3.02a07_mingw32_nt_1.0.17-0.48-3-2-_i686_i686, libparanoia sup
port
AUDIOtrack pre-emphasis copy-permitted tracktype channels
 1-15 no no audio 2
Table of Contents: total tracks:15, (total time 57:28.72)
 1.( 3:50.00), 2.( 4:16.50), 3.( 3:59.45), 4.( 3:28.52), 5.( 3:44.08),
 6.( 3:12.20), 7.( 3:52.20), 8.( 3:45.20), 9.( 4:08.45), 10.( 4:00.65),
 11.( 3:58.67), 12.( 3:50.70), 13.( 3:50.15), 14.( 3:08.60), 15.( 4:21.60),

Table of Contents: starting sectors
 1.( 0), 2.( 17250), 3.( 36500), 4.( 54470), 5.( 70122),
 6.( 86930), 7.( 101350), 8.( 118770), 9.( 135665), 10.( 154310),
 11.( 172375), 12.( 190292), 13.( 207612), 14.( 224877), 15.( 239037),
 lead-out( 258672)
CDINDEX discid: Bk20VmlSPt6WEen5bpehBUBc6no-
CDDB discid: 0xc20d780f
CD-Text: not detected
CD-Extra: not detected
No media catalog number present.
scanning for ISRCs: 15 ...
index scan: 1...difference: TOC:without, subchannel:with preemphasis
correcting TOC...
difference: TOC:with, subchannel:without preemphasis
correcting TOC...
index scan: 5...difference: TOC:without, subchannel:with preemphasis
correcting TOC...
difference: TOC:with, subchannel:without preemphasis
correcting TOC...
difference: TOC:without, subchannel:with preemphasis
correcting TOC...
index scan: 6...difference: TOC:without, subchannel:with preemphasis
correcting TOC...
difference: TOC:with, subchannel:without preemphasis
correcting TOC...
index scan: 8...difference: TOC:without, subchannel:with preemphasis
correcting TOC...
difference: TOC:with, subchannel:without preemphasis
correcting TOC...
difference: TOC:without, subchannel:with preemphasis
correcting TOC...
index scan: 9...difference: TOC:without, subchannel:with preemphasis
correcting TOC...
difference: TOC:with, subchannel:without preemphasis
correcting TOC...
index scan: 10...difference: TOC:without, subchannel:with preemphasis
correcting TOC...
difference: TOC:with, subchannel:without preemphasis
correcting TOC...
difference: TOC:without, subchannel:with preemphasis
correcting TOC...
index scan: 11...difference: TOC:without, subchannel:with preemphasis
correcting TOC...
difference: TOC:with, subchannel:without preemphasis
correcting TOC...
index scan: 15...

前面的不重要,不过也可以看到TOC里没扫到pre-em。但是从每个track的subcode scan开始:

index scan: 1...difference: TOC:without, subchannel:with preemphasis
correcting TOC...
difference: TOC:with, subchannel:without preemphasis
correcting TOC...
index scan: 5...difference: TOC:without, subchannel:with preemphasis
correcting TOC...
difference: TOC:with, subchannel:without preemphasis
correcting TOC...
difference: TOC:without, subchannel:with preemphasis
correcting TOC...

这就很诡异了:扫第1轨的时候,显示发现了TOC和subchannel不一致(subchannel=有,pre-em,TOC=无),于是纠正TOC,到这步没问题;但是紧接着,居然又来了一遍,不过这次是说TOC有(废话,你自己刚改的),subchannel没(??),于是又改了一次,把TOC给改回成无了!

同理第5轨,不过这次不同的是这么翻来覆去改了三次,即TOC为无->有->无->有,最后结论是有pre-em。

那么上面的全部总结下来,最后留在.inf文件里的居然是:tr. 5/8/10有pre-em。发现问题没有?正好和之前用CUERipper测得的向前偏移了一轨(1前面是0所以没有了)。为了保险起见,我又把这几轨全部找出来,和其他碟里收录的同一轨进行反复对比。虽然有的确实比别的版本稍微锐那么一点点(例如tr. 10那首「シンデレラたちへの伝言」我手头有俩无损版本的,有一个还是我自抓),但是如果真的去跑de-em,那又钝太多(靠频谱),所以基本可以90%肯定是没有pre-em的。

好家伙,Linux下最有名DAE(digital audio extraction)软件的cdda2wav居然有毛病,这事儿我得去hydrogenaudio反馈一下了。那让我们看看cdrdao好了。这里先说个笑点:EAC的目录里赫然有个cdrdao.exe(虽然是很老的1.1.9版),可见他也是调用了cdrdao来进行TOC提取的……然而阉割了cdrdao带的从track subcode里提取信息的功能。这软件的Windows binary也是相当的难找,最后在这个已经404的网页Archive里找到了一个编译好的1.2.3版(2009年最后版)和相应的dll文件(其实就是上面提过的cygwin1.dll)。

废话不多说,上log。

E:\sync\Software\cd\cdrdao-1.2.3-bin\cdrdao-1.2.3-win32>cdrdao read-toc --device
 8,0,0 test.toc
Cdrdao version 1.2.3 - (C) Andreas Mueller <[email protected]>

8,0,0: HL-DT-ST DVDRAM SP80NB60 Rev: RA00
Cannot read driver table from file "/usr/local/share/cdrdao/drivers" - using bui
lt-in table.
Using driver: Generic SCSI-3/MMC - Version 2.0 (options 0x0000)

ERROR: Unable to determine drive letter for device 8,0,0! No OS level locking.
Reading toc data...

Track Mode Flags Start Length
------------------------------------------------------------
 1 AUDIO 0 00:00:00( 0) 03:50:00( 17250)
 2 AUDIO 0 03:50:00( 17250) 04:16:50( 19250)
 3 AUDIO 0 08:06:50( 36500) 03:59:45( 17970)
 4 AUDIO 0 12:06:20( 54470) 03:28:52( 15652)
 5 AUDIO 0 15:34:72( 70122) 03:44:08( 16808)
 6 AUDIO 0 19:19:05( 86930) 03:12:20( 14420)
 7 AUDIO 0 22:31:25(101350) 03:52:20( 17420)
 8 AUDIO 0 26:23:45(118770) 03:45:20( 16895)
 9 AUDIO 0 30:08:65(135665) 04:08:45( 18645)
10 AUDIO 0 34:17:35(154310) 04:00:65( 18065)
11 AUDIO 0 38:18:25(172375) 03:58:67( 17917)
12 AUDIO 0 42:17:17(190292) 03:50:70( 17320)
13 AUDIO 0 46:08:12(207612) 03:50:15( 17265)
14 AUDIO 0 49:58:27(224877) 03:08:60( 14160)
15 AUDIO 0 53:07:12(239037) 04:21:60( 19635)
Leadout AUDIO 0 57:28:72(258672)

PQ sub-channel reading (audio track) is supported, data format is BCD.
Raw P-W sub-channel reading (audio track) is supported.
Cooked R-W sub-channel reading (audio track) is supported.
Analyzing track 01 (AUDIO): start 00:00:00, length 03:50:00...
Found 38 Q sub-channels with CRC errors.
WARNING: Pre-emphasis flag of track differs from TOC - toc file contains TOC set
ting.
Analyzing track 02 (AUDIO): start 03:50:00, length 04:16:50...
Found pre-gap: 00:02:10
Found 28 Q sub-channels with CRC errors.
Control nibbles of track match CD-TOC settings.
Analyzing track 03 (AUDIO): start 08:06:50, length 03:59:45...
Found pre-gap: 00:02:08
Found 21 Q sub-channels with CRC errors.
Control nibbles of track match CD-TOC settings.
Analyzing track 04 (AUDIO): start 12:06:20, length 03:28:52...
Found pre-gap: 00:02:00
Found 11 Q sub-channels with CRC errors.
Control nibbles of track match CD-TOC settings.
Analyzing track 05 (AUDIO): start 15:34:72, length 03:44:08...
Found pre-gap: 00:01:72
Found 9 Q sub-channels with CRC errors.
Control nibbles of track match CD-TOC settings.
Analyzing track 06 (AUDIO): start 19:19:05, length 03:12:20...
Found pre-gap: 00:02:03
Found 12 Q sub-channels with CRC errors.
WARNING: Pre-emphasis flag of track differs from TOC - toc file contains TOC set
ting.
Analyzing track 07 (AUDIO): start 22:31:25, length 03:52:20...
Found pre-gap: 00:02:03
Found 20 Q sub-channels with CRC errors.
Control nibbles of track match CD-TOC settings.
Analyzing track 08 (AUDIO): start 26:23:45, length 03:45:20...
Found pre-gap: 00:02:03
Found 17 Q sub-channels with CRC errors.
Control nibbles of track match CD-TOC settings.
Analyzing track 09 (AUDIO): start 30:08:65, length 04:08:45...
Found pre-gap: 00:02:03
Found 13 Q sub-channels with CRC errors.
WARNING: Pre-emphasis flag of track differs from TOC - toc file contains TOC set
ting.
Analyzing track 10 (AUDIO): start 34:17:35, length 04:00:65...
Found pre-gap: 00:02:05
Found 10 Q sub-channels with CRC errors.
Control nibbles of track match CD-TOC settings.
Analyzing track 11 (AUDIO): start 38:18:25, length 03:58:67...
Found pre-gap: 00:02:03
Found 15 Q sub-channels with CRC errors.
WARNING: Pre-emphasis flag of track differs from TOC - toc file contains TOC set
ting.
Analyzing track 12 (AUDIO): start 42:17:17, length 03:50:70...
Found pre-gap: 00:02:02
Found 11 Q sub-channels with CRC errors.
Control nibbles of track match CD-TOC settings.
Analyzing track 13 (AUDIO): start 46:08:12, length 03:50:15...
Found pre-gap: 00:02:00
Found 9 Q sub-channels with CRC errors.
Control nibbles of track match CD-TOC settings.
Analyzing track 14 (AUDIO): start 49:58:27, length 03:08:60...
Found pre-gap: 00:02:00
Found 7 Q sub-channels with CRC errors.
Control nibbles of track match CD-TOC settings.
Analyzing track 15 (AUDIO): start 53:07:12, length 04:21:60...
Found pre-gap: 00:02:00
Found 6 Q sub-channels with CRC errors.
Control nibbles of track match CD-TOC settings.

Reading of toc data finished successfully.

可以看到,找到且只在1/6/9/11轨中找到了pre-em信息,和CUERipper一致。不过有一点,他只是提醒你“WARNING: Pre-emphasis flag of track differs from TOC – toc file contains TOC setting.”,实际在提取出来的TOC文件里并自动没有修改成正确的、来自subcode的pre-em tag,这个还得自己手动来。

部分Pre-em的CD rip的存储和回放方式

碟算是抓完了,pre-em信息也有了,这就引出下一个问题:如何存储/回放这样的碟。WAV+CUE的方式是行不通的——因为上篇说过了,外置CUE里的FLAG PRE foobar2000不认,你也无法给CUE加pre_emphaiss的tag,加到音频文件里又会适用给整张碟。

保持整轨的前提下,唯一的解决方案就是FLAC+内嵌CUE了(其他无损格式应该也行)。要注意的是,FLAC+内嵌CUE,并不仅仅等价于“FLAC+CUE,只不过CUE内嵌”。在这里,内嵌的CUE主要起一个提供时间戳分轨的功能,但是实际上你对每一轨的metatag的自由度非常高,和单独的FLAC没有两样。也就是说,你完全可以做到给单个FLAC文件里每一轨加不同的pre_emphaiss的tag——即使CUE不支持。你甚至可以做到:每一轨的album不一样!不过如果你真的这么做,然后用foobar2000带的内嵌CUE查看器你会发现,CUE里的album还是老的值,可见这些元数据里并不是真的存在CUE的(不过,如果你进行一些CUE能支持的metatag操作,比如同时修改所有轨的albumalbum artist,或者单独轨的artisttitle,内嵌的CUE还是会跟着同步更新的)。

这个方案算是不错,但是和直接用分轨FLAC+pre-em tag一样都有一个问题前面提过:RG。因为我有一些lossy的需要加pre-em tag的文件,所以我被迫用了DSP版的de-em;但是DSP和RG不兼容(计算RG时不考虑DSP)。所以我现在俩抉择:1) 转成分轨FLAC,且同时直接将de-em硬计算进去;2) 换回后处理版的de-em,但是把那些有损音频强行transcode成无损一份来存储。实话说,两种方案都很蠢。现在我暂时还没想好到底要用哪种方案,姑且先去foobar论坛发了一贴问。

更新:今天又发现一个foo_deemph的bug:丫不支持FLAC+内嵌CUE的pre-emphasis tag,加了也不会de-em,orz 所以指望靠FLAC+InCue来保持最大程度的原Image是不现实了。我现在采用的方案是:使用foo_deemph插件,但是把所有 1) 只有部分轨有pre-em的CD 2) 有损带pre-em的音频 全部转一份分轨FLAC然后加pre_emphasis = 1 的tag。老的文件打包备份(防止被foobar20000的数据库读取;我在FB里filter掉了*.rar和*.zip)。这样的缺点是很占空间(有损转无损,还保留两份…)

OK,这个话题到此算是结束了。接下来我得强迫症般地把之前抓的碟都拉出来看一看,以保证没有subcode的pre-em flags……

收集老CD要注意Pre-emphasis的存在

Blog荒废了好几个月,本来正好前天有个题材(压片相关)要写,结果正巧碰到另外个事儿,觉得得先写写这个。无损音乐自己玩了这么多年,以为算是太阳下面没什么新鲜事,没想到上次一个Replay Gain一个Media server把我好好折腾了一番。这不,今天又接触了一个前所未闻的概念——emphasis。

这个问题其实要从几个月前说起。当时我在收集小猫俱乐部相关CD的时候,下到这么一盘——うしろゆびさされ組的首专「ふ・わ・ふ・ら」。

うしろゆびさされ組 - ふ・わ・ふ・ら

当然,这种80年代的老碟都是基本只能找到MP3或者网易云音乐(这里顺便提一句,网易云绝大多数老一些的、尤其是非华语音乐资源都是网上找的,那么多码率选项显然都是二次转换出来的。那伪320kbps实际一般看频谱都是128kps级别,还不如直接128呢,少一次有损压缩),我倒也没要求太高。

但是这张碟的奇怪之处在于,听起来非常“不舒服”。什么程度呢?我听完一张甚至会觉得耳朵疼(我听音乐一般声音很小)。我自认对这些东西比较敏感,觉得这碟肯定是哪里不对。我找遍网上所有的版本,都有这个问题。其中比较明显的是其中收录的单曲1、2,「うしろゆびさされ組」和「バナナの涙」,和单曲版一比就能发现明显区别。

不过,后来我在维基百科看到,说是其中收录的单曲曲目虽然未标记但是是Album version,所以这也算解释的通。不过,这事儿我一直放在心里没忘。

后来,在网上漫无目的地搜索下不到的「AN bALANCING TOY」时,发现骏河屋有人卖二手的「ふ・わ・ふ・ら」等其他几张指指点点组的专辑,外加写真集什么的。正好从来没在骏河屋里买过东西,也试试代购靠不靠谱,就准备把上面有的东西都一起扫了。国内的几个,2poi啊玛莎多拉啊啥的试了下,总有支付或者快递之类的种种问题,于是转投面向西方的服务。最开始试的就是在骏河屋下面打广告条的“Buy Smart Japan”,网站倒是非常简单好用,全自动和亚马逊差不多,不过快递只支持EMS,那4500円的运费把我吓到了(毕竟日亚直购运费便宜的一笔)。

于是我又找了另外一家,Proxyrabbit Japan。这个的界面就蠢多了,买骏河屋一次最多5件,而且你填完表格之后他是通过人工确认和你邮件交流……并且不能即时出运费价格,要先交定金(最低25%商品价格)然后他那边到货了才告诉你邮费。不过我当时抱着上当一次也无所谓的心理就上了。大概quote后过了2天我交了定金(PayPal),又过了一周多那边才收到(据说骏河屋就是挺慢?)货。运费这次倒是有几个档可以选,结果EMS还是4500,于是我选了两千多的Air mail。

废话不多说,总之就是昨天才到我家,还没投递成功我去邮局自取的,前前后后快一个月了。以后还有需求的话,估计会试试转运而不是代购的(看到推荐了两家,http://www.biginjap.com 和 http://www.tenso.com/en/)。二手卖相还可以,没侧标就是了。

IMG_20170621_220513

EAC的话我虽然用得很少,前个月倒也抓过几次,我的经验就是没必要看什么“教程”,按照软件自带的指引一步步点,把offset之类的硬件相关设好就OK。我当年第一张是用的安全模式抓的,速度慢到发指(1.几x),我于是直接改用爆发模式抓(速度7-8x),反正抓完之后有AccurateRipCTDB两个数据库比较CRC32,如果都一致,都是bitwise identical了还觉得只有安全模式才行那只能认为是没文化了。这里容我吐槽下,都CURRENT YEAR了EAC抓出来的CUE居然还只能是ANSI(非Unicode)的,蠢哭。

总之……回到正题。于是我自然先赶紧抓了这张碟来听。嗯?听感好像和之前的完全一致,看来这碟本身就这样?就在我放弃治疗去改CUE的时候,发现有个没见过的玩意:

REM DATE 1986
REM DISCID 9109350A
REM COMMENT ExactAudioCopy v1.3
PERFORMER "うしろゆびさされ組"
TITLE "ふ・わ・ふ・ら"
FILE "うしろゆびさされ組 - ふ・わ・ふ・ら.wav" WAVE
 TRACK 01 AUDIO
 TITLE "SE・KI・LA・LA"
 FLAGS PRE
 INDEX 01 00:00:00

FLAGS PRE是什么鬼?

结果一查不得了,这正是问题的关键。原来,通信中存在一个叫做“emphasis”的技术。说来也很弱智,就是对于多少频率的信号(一般是高频),会进行放大;在信号到达接收端之后,再用反函数把信号衰减回去。放大的过程叫“pre-emphasis”,衰减叫“de-emphasis”。不用说,其目的是减少传输过程中噪音的影响,因为中间引入的噪音会被同样衰减,从而达到更高的信噪比。

这个技术在黑胶唱片、磁带等Analog领域广泛应用,但是在CD这种数字媒体中的应用很少。准确地说,在早期CD(70-80年代)只有14bit的时候,这个应用在其中来减少量化误差(为什么?留作思考题了w)。后来CD的精度变成16bit之后,就基本很少用了,但是其依然存在于CD的标准中,想用的话也是可以的。

事实上,带pre-emphasis的CD恰恰就仅在日本的80年代最流行(注意,日本的CD从80年代后半才开始普及,之前基本发音乐只发黑胶、磁带),其他国家不多。另外需要说明的是“pre-emphasis”(即:特定高频的信号的增益)是直接压在CD里的,不过一般同年代的CD player都会识别metatag里的相关flag,从而自动进行进行de-emphasis来还原信号。

但是到了电脑光驱,这就取决于你的软件端怎么处理了。我们上面可以看到,对于EAC抓轨,他会识别出相关的tag,并标记在CUE中。接下来就全看你的播放器了。

对于绝大多数人,和绝大多数软件,他们是完全无视这个信息的,所以就导致了我最开始提到的那张碟“很怪”的声音——高频被增益了(具体算法根据hydrogenaudio,是“a first-order filter with a gain of 10 dB (at 20 dB/decade) and time constants 50 μs and 15 μs”,我虽然大概能理解但是就不硬着翻译了,毕竟不是科班出身),所以听着耳朵疼,架子鼓里的钹的声音也变了。另外因为这个东西的存在过于冷门,大多数人估计根本没处理过,这也是为什么网上流传的版本全是这个德行的原因。仔细想想可怕极了,到底有多少手里的80s音乐会有这种问题而没被发现呢?

既然知道了原因,解决方案就很简单了。用Foobar2000的话,有网友开发出两个插件来处理这个问题——一个是后处理,一个是DSP插件,前者会自动在播放、计算音量增益、转换(前提你勾选了后处理)时进行de-emphasis,后者顾名思义需要在DSP里作为一个滤镜勾选。需要注意两个默认都不工作,需要对音频文件(而且前者仅限无损)添加PRE_EMPHASIS的tag,值为“1”,“ON”或者“Yes”。

这里有几个细节我啰嗦几句。一个就是CUE里的FLAGS PREFoobar是完全无视的,所以对于无损+CUE的方案,你要把上述的Tag加到你的无损文件里去。另外一个就是转换时,如果勾选了后处理(你一般应该勾选)会自动把de-emphasis给硬编码进去,所以出来的output是已经进行过de-emphasis的了。但是因为转换器默认会把tag都复制到新文件里,所以新文件还有PRE_EMPHASIS的tag,需要手动移除,否则就相当于de-emphasis两次,声音就变钝了。另外,虽然可能性很小,万一有别的软件支持识别CUE的flag,那么播放CUE+有PRE_EMPHASIS tag的无损音频可能会导致多重de-emphasis?嘛估计应该没软件这么蠢就是……

其他的处理软件比较著名的有命令行工具sox,用sox input.wav output.wav deemph可以轻松de-emphasis。虽然和foobar插件两者的结果并不是波形级一致,但是听感无区别。

至于我这张CD的处理方法自然可以是两种,一个就是直接硬编码进去,另外一个就是播放的实时进行de-emphasis。我暂时选的是第二种,这样就不用保留一个单独的原版WAV了,毕竟是破坏性操作;因此,转WAV为FLAC(节省体积)的时候要特别注意去掉勾选“后处理”选项,或者先转换格式,后加pre-emphasis tag。

最后,以这张专辑中我最喜欢的一首歌曲,『偏差値BOY』作为结尾吧 😀

利用MATLAB来进行批量图片颜色匹配

Edit: FML,回到家才发现用MATLAB处理出来的颜色和死了妈似的,学校的垃圾TN显示器太坑了……下文已经尽量修正。

在图像处理尤其是对冻鳗扫图处理时,“颜色匹配”是一个经常需要进行的操作。因为印刷和扫描的技术和载体所限,扫描出来的结果经常和艺术家想呈现的有很大偏差。当然,最理想的纠正方式应该是从源头上去校准扫描仪,但是很多时候这是不现实的(例如,图片来自网络)。

在有官方提供的Sample的情况下(需要注意:在官网、亚马逊等地方获取样图时,诸如CMYK硬拉成RGB之类的错误屡见不鲜,一定要避免从源头上就选了个错误图片做reference),利用数字原档来校准、还原颜色就成为了可能。

Photoshop有个内建的匹配颜色(英文:Match color)功能:

2017-04-21

可以看到,功能还算丰富。除了上面的滑块选项以外,你可以选择用于计算的选取范围(包括源和目标的都能选),最后计算出来的映射/变换则可以应用到全图(最上面那个现在灰色的选项),而不是仅限于选区。

PS的颜色匹配效果还凑合,但是为了追求最佳效果,在匹配之前一定要尽量把图像对齐。如果两者的范围不一致,也要把多出的部分裁掉。还好,这个过程在PS里也很简单:

  1. 导入两张图片为两个不同的图层。你的扫图置于底层(底层是不会变形的),样图置于上层。注意,不能是“背景”,所以如果是的话,双击一下转换为图层先。
  2. 选中两个图层,然后点Edit->Auto align layers。
    • Projection选Auto就好,下面两个透镜扭曲修正的选项不用勾。确定之后过几秒,应该就对齐了。PS这个算法不是那么完美,尤其是在图像有缩放的时候做不到pixel级对齐,不过对于我们这里的应用足够了。
    • 如果样图太小,可能会无法自动对齐。这种情况下,预先用自由变换(Ctrl+T)调整一下样图的大小吧。记住拖拽时按住Shift可以保持宽高比。你不用刻意调整和你的扫图完全一样大,差不多随便拖一下就行,因为自动对齐时PS会自动帮你做。
  3. 把两者不一致的地方裁剪掉。也很简单:ctrl+点击第一个图层,会建立一个和该图层一样大的选区;然后Crop就好。如法炮制第二个图层。这样,剩下的部分就是两个图层都有的部分。如果只有一个图像上有水印、文字之类的,最好把那部分也裁掉,否则影响颜色匹配。

接下来,只要进行颜色匹配就行了,注意源和目标别选反就行。

但是这里一个问题:如果你有一组扫图,其色彩基本类似,理论上而言,你应该只需要找其中一张的sample匹配,然后把那个transofrmation存下来,适用到其他图即可。可惜,PS并不提供这样的功能。当然,有一个笨方法:先把所有图片拼到一起,然后只选中其中一张图的区域进行颜色匹配,但是应用时忽略选区。但是这个方法实在听上去太蠢了还不能自动化。

于是,我转而追求其他的办法。MATLAB的Image processing toolbox集成了许多现成的图像处理函数,当年学DIP的时候也用得不少,于是我第一个就想到它。随便找一下,这个Histogram matching(imhistmatch)似乎就符合我的需要。

虽然不知道PS的图像匹配是什么原理(毕竟是商业机密),但是我约莫着其实也八九不离十也就是histogram matching。说白了,就是找到一个映射,使得变换后的直方图累计分布函数(CDF)和reference相似。我先试了下MATLAB这函数的效果,嘿还不错,甚至可以说比PS还强一点:

compare

(点击大图,注意观察云彩的颜色)

修正!PS的效果其实秒杀MATLAB的…只是由于学校的垃圾屏幕太烂我才没看出来orz 不过后面的内容姑且保留了,从RGB改用YCbCr效果会好很多。

既然我们找到这个函数,那现在就只需要剖析一下他(选中函数ctrl+D可以看源代码),看看里面到底是怎么运行的,这样我们就能把那个transofrmation给导出来。

结果这个imhistmatch的代码相当简单,其核心部分居然是调用另外一个函数——histeq,即直方图均衡化(Histogram equalization)。其实也很好理解,直方图均匀化本来就是直方图匹配的一个特例——即匹配出来的目标CDF是一条直直的斜线。在MATLAB里,两者都集成到了histeq而不是imhistmatch里可能也是因为前者叫起来更顺口吧。histeq除了直接均衡化之外,还可以手动设一个作为目标/参照的直方图来match,所以这里就是把我们的目标/参照的直方图拿来作为input了。需要注意histeq只支持单通道(准确地说是imhist只支持单通道),所以需要手动写个矩阵运算或者循环来分别搞RGB。

那么,histeq里面的原理又是怎样呢?文档里可以看到,其实这个函数本身就可以返回一个变换T给你了。这个变换T的形式是1×256个double array,每个分别是一个0-1闭区间内的值。用法嘛其实不言而喻了,就是用每个位置对应的double去乘原图的[0, 255]的强度值,得出来的就是output,其实和PS里的Curve是一模一样的。不过,这里可以偷个懒,PS有个内部函数grayxformmex就是帮你算这个的,会自动处理所有支持的图像数据类型(从double,uint8到uint16),而且是用C/C++写的速度更快,推荐直接用它。不过这个内部函数不在path里所以不能直接用,你得先复制grayxformmex.mexw64到你得工作目录才行(是编译过的文件,源代码如果感兴趣的话,网上能找到一个老版本的)。

OK,有了这一切东西,我们就可以很简单地写一个script:


color_mode = 'ycbcr'; %rgb, ycbcr, hsv
work_patch = 'E:\!Scan\image match\';
ref = imread([work_patch,'ref.bmp']); % Your reference image
A = imread([work_patch,'img.bmp']); % Your scan that needs to be calibrated
numColorChan = size(ref,3);
isColor = numColorChan > 1;
if isColor
switch color_mode
case 'ycbcr'
ref=rgb2ycbcr(ref);
A=rgb2ycbcr(A);
case 'hsv'
ref=im2double(ref);
ref=rgb2hsv(ref);
A=im2double(A);
A=rgb2hsv(A);
end
end
% Anything beyond 256 will essentially give the same result. If you want
% the color to be closer to your ref, you can choose lower value but
% obviously that will result in banding (because it also decides how many
% discrete colors your result will have.
N = 1000;
% Compute histogram of the reference image
hgram = zeros(numColorChan,N);
for i = 1:numColorChan
hgram(i,:) = imhist(ref(:,:,i),N);
end
% Adjust A using reference histogram
hgramToUse = 1;
for k = 1:size(A,3) % Process one color channel at a time
if isColor
hgramToUse = k; % Use the k-th color channel's histogram
end
for p = 1:size(A,4)
% Use A to store output, to save memory; T is the transformation
% matrix
[A(:,:,k,p), T(k,:)] = histeq(A(:,:,k,p), hgram(hgramToUse,:));
end
end
%%
% This is your image after matching.
if isColor
switch color_mode
case 'ycbcr'
A=ycbcr2rgb(A);
imwrite(A,'match_ycbcr.bmp');
case 'hsv'
A=hsv2rgb(A);
imwrite(A,'match_hsv.bmp');
otherwise
imwrite(A,'match_rgb.bmp')
end
else
imwrite(A,'match_grey.bmp');
end
%% Batch process other images
%myfolder='E:\!Scan\_temp\backup'; % load other images E:\!Scan\_temp\done
myfolder=[work_patch,'todo'];
newfolder =[myfolder,'_changedcolor']; % result folder
mkdir(newfolder);
files = [dir(fullfile(myfolder,'*.jpg')); dir(fullfile(myfolder,'*.bmp')); dir(fullfile(myfolder,'*.png'))];
for file = files'
fullfilename = fullfile(file.folder, file.name);
new=imread(fullfilename); % Now load another image.
switch color_mode
case 'ycbcr'
new=rgb2ycbcr(new);
case 'hsv'
new=im2double(new);
new=rgb2hsv(new);
end
for k = 1:size(A,3)
% Use the same transformation. make sure you have
% grayxformmex.mexw64 in your working path.
new(:,:,k)=grayxformmex(new(:,:,k),T(k,:));
end
switch color_mode
case 'ycbcr'
new=ycbcr2rgb(new);
case 'hsv'
new=hsv2rgb(new);
end
imwrite(new,fullfile(newfolder,[file.name,'.bmp']));
end

view raw

image_match.m

hosted with ❤ by GitHub

我尽量模仿了imhistmatch的格式,所以应该对多种数据类型都能处理。不过可能数据验证方面的鲁棒性会差一点。最后%%后的部分是批量处理其他文件的代码。

由于前面红字所说的原因,我紧急改用了YCbCr channel来进行处理,效果确实好很多,但是天空的颜色果然就不对了orz,而且还是不如PS的…HSV我也试了,结果会出现奇怪的色块,懒得深究了。上面的代码已经更新,可以选择用RGB(默认),YCbCr或是HSV(开关在最上面)。对比一下三种的效果:

compare2.png

不过需要注意,MATLAB的算出来的结果颜色有点离散,纯色区域多多少少有些banding。这点PS的祖传算法就略胜一筹了,出来的图像的直方图还是非常连续的,没有太多的banding的问题。不过后来发现其实只有RGB模式会有这个问题,换成YCbCr就基本无大碍了。无论如何,这里推荐俩解决办法:方法1,先跑色彩匹配,再进行其他去噪步骤,会很大地由于去噪中的模糊等因素缓解这个问题;方法2,加点白噪声先,至少观感会强很多。这两种方法可以结合使用。

另外,PS的明度控制明显要好很多,我用YCbCr跑出来的结果高光区域都变淡了(比如有几页的纯白背景)。

2017-05-21补充:

后来我又多次使用上文所述的方法进行校色。我发现这个方法其实还是相当可行的,在很多时候甚至比PS的结果要好。不过要注意的是,即使是用YCbCr,有时候(很少见)也会出现和HSV一样的色块问题,所以出了结果一定要检查一下。

关于JPEG的那点事儿 Part 2:JPEG原理

前言

本文其实于差不多正好1年前写成,是关于JPEG的那点事儿的补充。但是由于实战篇一直烂尾,拖到现在。前几天看到Google发了个JPEG新算法,说是可以将JPEG的体积同质量情况下再压缩35%,突然想起了这文了。为了说清楚Google为什么能在古老的JPEG上压榨出新的空间,我觉得还是有必要先讲清楚JPEG的原理。但是本文成文之后实在太长,所以我想了想还是把和Google算法相关的、以及一个TL;DR版的JPEG原理单独发文(大概明天8点发w)。另外,前面提到的“实战篇”也会分割放送,减少文章长度。

序言

有一位朋友看了上文后问到,为什么步进(progressive)JPEG可以提高压缩率?

严格来讲,步进(Progressive)和交错(Interlacing,虽然“交错”是最常用的翻译,但是我是在无法完全理解这两个字的汉字想表达啥…)并不是一个概念。要讲步进,得先讲讲交错。

交错指的是图像解码时(以及存储时)并不是按照某个逐个像素依顺序解码——而是采用跳跃的方法:例如将整个图像分割成九个区域,先解码出每个区域的大体形状,然后再逐步解码细节。一般而言,这样的层级解码会分不止2层,例如在PNG使用的Adam7算法中,一共有7个子图像会被存储起来。维基百科上这张图可能会更直观一些:

https://upload.wikimedia.org/wikipedia/commons/2/27/Adam7_passes.gif

(From Wikimedia Commons)

这种方式的好处是,在图像加载过程中,图像会由模糊(准确地说是马赛克状)逐渐变清晰,而不是从上到下一行行地显示。这样在图像加载中途读者就可以对图像大概有个概念,而不是只能看到上面完全看不到下面:观感上加载速度会变快,而且也更方便一些。

你肯定要问了,这么做不是相当于在原图上在集成几个不同尺寸的略缩图,体积不增加就不错了,怎么会减小呢?事实上,对于其他图片格式,例如PNG和GIF,如果开启“交错”选项,确实图像会变大。但是对于JPEG的具体实现,所谓的“ progressive”,情况又不太一样。用IJG官方的FAQ里的话说“Basically, progressive JPEG is just a rearrangement of the same data into a more complicated order.”。但是具体技术上的实现方式是什么?

要说清这事儿可能还真得从头说一下JPEG的压缩过程。既然要讲,那就讲的详细一点。接下来我将把JPEG压缩的每个细节步骤(除了DCT的数学原理,这个我真不行……)都讲清楚。如果只是想了解大概,维基百科就写的不错了:但是如果真想做到自己写一个解码编码器的程度,有些细节不厘清还真不行。

JPEG编码基本原理

在DCT之前

JPEG编码主要分成三步,DCT、量化以及无损压缩。不过,在DCT之前,还要先色彩空间转换和色度抽样。色彩空间转换干的就是将RGB转换成YCbCr——即将亮度(Luma)和色度(Chroma)分离开,其理念是人眼对亮度的变化远敏感于色度变化等一大套感知视觉理论,这里不再赘述。顺便一提,色彩空间转换并不是完全无损的——因为转换前后都是整数,自然不可避免会有舍入误差。

转换之后,既然我们知道亮度更敏感,那就有做文章的空间。所谓色度抽样,就是对色度的部分进行抽样/缩小。主流的抽样方法有4:2:2、4:2:0,在JPEG的语境下更多叫做2×1和2×2,前者指水平分辨率抽样一半,垂直不变,后者指水平垂直各抽样一半。完全无抽样的叫做4:4:4,或者1×1。至于具体抽样缩图的算法JPEG里好像没有定义,一般都是直接将相邻两个像素求个平均了事(这里可能会导致图像处理界另一个著名的历史遗留问题:线性vs非线性色彩空间,以后抽空再单独写一下)。

抽样完毕后,终于可以进行到真正的编码部分了。JPEG压缩时,先将原图分割成8×8的block进行编码,又叫“最小编码单元(Minimum Coded Unit,MCU)”。当然,如果你有用色度抽样,MCU的大小也会相应放大。例如,如果你用了4:2:0的抽样,那么MCU就会变成16×16——但是Cr和Cb的实质大小其实依然只有1个8×8,只是塞进去了4个8×8的Y罢了。在编码的时候,每个通道也是分开的,所以这样的MCU可以理解成6个不同的blocks就行(但是压缩完之后的数据顺序又有讲究,后叙)。下面,我用“block”来特指单个通道的8×8的单元,来和MCU区分。

DCT

接下来,我们要对每个block的像素值(8bit图像就是0-255了,接下来全部以最常见的8bit为例。JPEG标准额外支持12bit图像,但是主要用于医疗领域,普通情况极少有人用)进行偏置128之后(使其集中在0两侧,加快DCT运算)做2D DCT转换成频域。转换出的结果依然是一个8×8的矩阵,只不过每个数据点代表的是不同频率(准确地说是一组不同的pattern(见下图))的强度:左上是低频,越往右下越高频。

File:Dctjpeg.png

(From Wikimedia Commons)

所以DCT说白了就是把原图分拆成这些pattern的线性叠加。其中左上角的DC分量,可以近似理解为整个block的强度均值,剩下的则是高频(或称AC)分量。

量化

DCT这步数学上来讲(不考虑舍入误差)是可逆的,真正的有损编码的是下面的量化步骤。量化在这个语境下其实就是拿一个预设的系数矩阵(量化表)去逐一除之前得出的DCT矩阵——介于人类对低频比较敏感,细节可以适当丢失,这个表的原则是越往左上系数越小,越往右下系数越大。至于具体的数据,据说都是实验出来的,不同的软件可能不同。主流的libjpeg根据JPEG标准的推荐,提供了一套0(1?)-100质量分别对应的表,可能也是最常见的系数。当然,你也可以自定义系数,例如Photoshop内置的量化表就和别的软件大多不一样。ImpulseAdventure的作者提供了一份非常详尽的市面上常见软件、相机的内置系数表。表格里数据的总体大小决定了JPEG的质量——系数越大,质量越低。高质量的表可能系数都只有个位数(事实上,100%质量的量化表全是1),而低质量的,例如拿一个JPEG 50%质量的量化矩阵来说,左上角的DC分量的除数有16,而右下区的甚至高达100左右。想象一下去拿这个表去除DCT矩阵,除出来的结果再近似到整数,考虑到右下的高频AC分量本来强度就不高,除以100之后基本都肯定小于0.5了,也就是会被约成0。这么搞下来,整个表就会变成一个右下区几乎都是0,而其他区域数值也很小的矩阵。

Y和Cb/Cr会有不同的量化表(可以猜到,色度那张压缩更狠),这个表会被嵌到JPEG文件的头部中。通过表格的数据可以估算JPEG的当时编码时的质量。

无损压缩

之后就到了无损压缩的部分,也是整个JPEG编码中最麻烦的部分。首先又得学个新词儿——Interleaving。这个一般也翻译成“交错”……但是和上面提到过的Interlacing不是一回事。我们前面知道,每个MCU有少至3个、多至6个8×8的blocks。编码的时候,我们既可以按照MCU分类,一次编码完整个MCU的所有block再进行下一个、也可以采用别的方式,这里按下不表。不过,最常用的、baseline的方法是先按MCU归类,然后按照一个固定的顺序读取。如果是无色度抽样的3 blocks MCU,那就是YCbCr的顺序,如果有多个Y,那就是Y00/Y01/Y10/Y11(左上,右上,左下,右下)/Cb/Cr的顺序。

sequence_2x2

(From ImpulseAdventure)

这种“不停地在不同components交替取数据编码”的方式(components在这个语境下就是不同的通道,Y、Cb和Cr都分别是一个component)就叫做“interleaving”,这样的JPEG叫做interleaved JPEG。可以看到,绝大部分的baseline JPEG都是interleaved的。

让我们回到每个block里面。首先要将我们的64个分量1D化。我们这里并不是按行或者列的顺序排队,而是通过斜对角蛇形的方式,从左上逐渐跑到右下。

600px-jpeg_zigzag-svg

(From Wikimedia Commons)

其原因是:越靠近左上的频率越低,量化压缩之后也越可能不是0,这样排序之后便于下面的游程编码(RLE,run-length encoding)进行。

游程编码

所谓RLE,是一种无损压缩高重复率数据的算法,还是直接引用维基的例子好了:“举例来说,一组资料串”AAAABBBCCDEEEE”,由4个A、3个B、2个C、1个D、4个E组成,经过变动长度编码法可将资料压缩为4A3B2C1D4E(由14个单位转成10个单位)。”

至于JPEG中的实现说起来则比较麻烦。简单概括,对于AC分量,我们只描述非零的强度和他们的位置。具体来讲,就是把非零的coefficient转换成“前置0的数量+强度的比特数”的分类单元+具体强度的形式。而强度是0的AC,自然被包含在那“前置0”里面了。这样说可能太抽象了,举个例子。假设我们的AC1和AC3(分别是矩阵第一行第二排和第三行第一排)分别是-2和3,而AC2是零。那么,AC1就会被表示成

(0,2)(-2)

(0,2)乃是分类单元(categories),0表示前面有0个0(毕竟这是第一个),2表示后面跟的数据(-2)需要用两位(2bit)才能表示(下述)。同理,AC3前面有1个0(AC2),那么就会被表示成

(1,2)(3)

或者我们把前面俩改写成16进制,共同占用1个字节:因为后面的数据不会超过15位(F)(实际上不会超过14位,高位F的部分除了F0特殊定义之外,并用不到),零的数量虽然确实会超过15个,但是我们特别指定(15,0)(0xF0,有的地方称作ZLF,“zero run length”的意思)为用来表示16个连续的0。如果某个block最后部分全是零,可以提前输出(0x00,End-of-Block,EoB,无需跟强度值)来结束这个block。

DC分量处理

对于DC量,首先我们用和前一个block的DC分量的差分的方式来记录——这样做可以节省一部分体积,因为图像大多是连续的,相邻block的DC分量一般相差不大,这么操作可以大幅度降低DC分量(一般是最大的一个数,也需要最多比特)的比特长度。这种编码方式叫做Differential pulse-code modulation

另外,我们也要像AC一样,对每个强度量先指定其位长的分类单元(否则我们怎么知道读到哪里算完呢?):0表示该DC量是0、1表示该DC量的数据只有1位、2表示有两位、……直到15(或者F),然后再写我们的数值。例如,如果一个DC量是15,那么就会被表示成

(4)(15)

即(位长)(数据)的形式。因为15需要4位才能编码下(下述),所以是4。

编码为二进制及霍夫曼编码

接下来,我们要进行最后的终极编码,也就是将上面这一堆劳什子转换成二进制。

对于强度的部分,很简单,按照之前分配好的位数转换即可。但是别忘了,我们的数值是有符号整数,要转成无符号的0和1,所以要稍微偏移(shift)一下。如果数据是0,对于AC自然就直接跳过了,DC会被分配“0位”,也就是只有分类单元的部分,而并没有数据。分配1位时,0代表-1,1代表1;分配2位时,用00表示-3、01表示-2、10表示2、11表示3,以此类推。这个编码方式是JPEG标准指定的,所有JPEG都一样,不能自定义。其实,它是用补码(二补数)推算出来的:如果数据是正的,那么就取补码最后N位(位数前面分配了);如果数据是负的,就取补码最后N位再减一。举例子的话,15被分配4位,15的补码是0…00001111(具体有多少个前置零取决于你的比特数,但是这里不影响),取最后四位就是1111;如果是-15,补码是1..1110001,取最后4位再减一就是0000。到这里也应该看出来了,我们的位数也不是随便分配的,说白了就是对于绝对值处在[2^N, 2^(N+1))之间的数,我们分配N位来表示。具体可以参照这文中的Table 5。

不过对于前面的分类单元部分,则不是直接转换成二进制就算完了,我们要充分利用霍夫曼编码的方法进一步压缩。如果有不了解霍夫曼的,其实就是简单地重排数据编码方式,出现频率高的用更少的比特编码,频率低的用更多的来编码,并最终得到一个总比特数更短的二进制码。如果我们不用霍夫曼,可以看到上面的分类单元对于AC量每个有8位,对于DC也有4位。霍夫曼编码后,其长度变成2至十几位不等。

JPEG默认就是启用霍夫曼的,其标准中也有个推荐的霍夫曼码表。但是和量化表一样,你也可以自定义——事实上,有人就发现,Photoshop就有一套自己单独搞的霍夫曼码表,而且根据JPEG的质量不同还稍有不同,以求达到最佳的压缩效果。另外,DC和AC、Luma和Chroma都可以分别使用不同的霍夫曼表,也就是说一般会有四张霍夫曼表。该表显然也会和量化表一样存在header中,否则无法解码。

不过,最优的霍夫曼(在JPEG文件结构的限制内。有研究称JPEG的霍夫曼从设计上就不可达到最优)自然是对于每个JPEG单独统计每个分类单元出现频率然后构建霍夫曼表了:——这也就是一般软件保存JPEG时的“优化霍夫曼”或者“Optimize”的意思了。不过很显然,这样做会要求先对所有MCU进行一遍扫描,自然会降低编码速度(也需要更大的buffer,这点在设计encoder的时候需要注意)。

最后,这些纯二进制的数据会按照MCU1-Y-DC、MCU1-Y-AC、MCU1-Cb-DC、MCU1-Cb-AC、MCU1-Cr-DC、MCU1-Cr-AC、MCU2-Y-DC……的顺序拼在一起。整个数据块必须结束在整字节里,最后不足的部分补1。另外一个特殊之处在于,如果数据中某个字节出现了0xFF(1111 1111),为了防止和JPEG的marker(标示各个组成部分开始的标示)混淆(全部以x0FF+非0字节组成),会加入padding 0来改写成0xFF00。

讲到现在,终于把普通的baseline、sequential的JPEG编码原理讲完了。

Baseline?Sequential?

插播一段:关于“baseline”的定义,其实非常含糊。根据ITU的standard的术语表,baseline其实是和“extended”相对应的,而不是progressive——sequential才是。“extended”是指每次Scan(下述)可以有高达8张霍夫曼表、并且每个component的可以是12位等等的扩展格式(极为罕见)。但是,在统一标准中后文中又出现了baseline和progressive的相对应……总而言之,连ITU自己的文档里术语都不是很严谨。在民间使用时,baseline多用来和progressive相对(虽然sequential更合适一点),这点要搞明白。

步进(Progressive)JPEG

我们还完全没提到最重要的步进(progressive)到底是怎么回事。不过看到上一段我们应该能想到,由于DCT的特性,高低频的数据都已经分离出来了。如果我们在存储时并不是按照完全按照MCU的顺序,而是先把DC和一些序号较小的AC分量挑出来先存储,这样加载的时候不就可以做到从模糊到清晰的效果了吗?没错,这就是progressive JPEG的基本原理了。而且,这样做我们只是调整了同样数据的位置而已,理论上并不会增加体积。

在具体实现上,得先讲一下Scan的概念。JPEG中经过DCT和量化之后的那堆系数(就是那些8×8矩阵),可以分为多成多个Scan来保存——每个Scan中只分配、存储部分数据。具体的分配方式,可以分为三种:

  1. 按照component分开。还记得上面提过的interleaved的概念吗?一般的JPEG,是将三个components(Y、Cb、Cr)全部混在一起编码的。你也可以全部分开——每个Scan只处理一个component。
  2. 按照8×8 block中的序列号(依然是蛇形顺序)分开。
  3. 按照编码后(二进制)的强度量的比特位置分开。

其中,后两个又称作“progression”,采用这种方式来分scan的JPEG就是progressive scan了。

Spectral selection

采用第2种方式的,叫做“Spectral selection”。例如,我们可以单独压缩0(DC),然后是AC1-AC6,然后剩下的AC7-63再一起。这样在传输图像时,DC部分在第一个scan就会扫描到,后面的慢慢读取。不过具体分配方式并不是任意的,有以下规定(ITU T.81 G.1.1.1.1):1)DC和AC必须分开;2)只有DC的Scan可以是interleaved(包含多个components,也就是色度亮度一起编码),AC的Scan必须是只含有一个component。所以,更现实的分配方式可以是

# Interleaved DC scan for Y,Cb,Cr:
0,1,2: 0-0, 0, 0 ;
# AC scans:
0: 1-2, 0, 0 ; # First two Y AC coefficients
0: 3-5, 0, 0 ; # Three more
1: 1-63, 0, 0 ; # All AC coefficients for Cb
2: 1-63, 0, 0 ; # All AC coefficients for Cr
0: 6-9, 0, 0 ; # More Y coefficients
0: 10-63, 0, 0 ; # Remaining Y coefficients

这个范例引用自 libjpeg-turbo的wizard.txt,这也是cjpeg.exe支持的scan file的格式。其中的0,0部分,下面马上提到。

Successive approximation

采用第3种的,叫做“Successive approximation”(有的地方叫做Successive renement,而把两者共用叫做Successive approximation……没错就是这么混乱)——所谓按照比特位置,就是把每个分量系数(coefficient)的二进制强度(或者称值)按照高位低位分开。例如,假设我们的值都是8 bit,我们可以在第一次scan只传输前7位,最后一次scan再把最低一位(Least significant bit,LSB)传输——可想而知,其效果就是图片精度逐渐变高了。上图的0,0部分的意思就是没有successive approximation。

这么说可能还是觉得有点迷茫,ITU T.81标准中的这张图可能是最直观的了:

itu-t81 124

(From ITU T.81)

Spectral selection和Successive approximation可以结合起来一起用。例如,cjpeg默认的-progressive采用以下这样的scan file:

# Initial DC scan for Y,Cb,Cr (lowest bit not sent)
0,1,2: 0-0, 0, 1 ;
# First AC scan: send first 5 Y AC coefficients, minus 2 lowest bits:
0: 1-5, 0, 2 ;
# Send all Cr,Cb AC coefficients, minus lowest bit:
# (chroma data is usually too small to be worth subdividing further;
# but note we send Cr first since eye is least sensitive to Cb)
2: 1-63, 0, 1 ;
1: 1-63, 0, 1 ;
# Send remaining Y AC coefficients, minus 2 lowest bits:
0: 6-63, 0, 2 ;
# Send next-to-lowest bit of all Y AC coefficients:
0: 1-63, 2, 1 ;
# At this point we’ve sent all but the lowest bit of all coefficients.
# Send lowest bit of DC coefficients
0,1,2: 0-0, 1, 0 ;
# Send lowest bit of AC coefficients
2: 1-63, 1, 0 ;
1: 1-63, 1, 0 ;
# Y AC lowest bit scan is last; it’s usually the largest scan
0: 1-63, 1, 0 ;

可以看出,整个过程有高达10个Scan。第一个Scan输送所有component的DC分量(除了最后一个bit,即LSB);然后输送Y通道的前5个AC分量,不过不包含最后两个bit;接下来是Chrma的所有AC分量,但是不包含最后一个bit(如上所述,在progressive中除了DC分量其他的都不允许interleaving,所以分了两次scan;而且,这里选择了先scan了Cr,因为人眼对Cb最不敏感)。再接下来是Y通道的后面6-63个AC分量,依然不包含最后两个bit。再下来是Y通道所有AC分量(1-63)的倒数第二个bit。

最后4个scan就是重复上面的顺序将所有的通道、分量的最后一个bit给传送了。

其他细节

至于在具体实现方式上,Spectral selection倒是蛮好理解的,每处理到某个MCU只要只处理其中一部分分量就是了。但是由于每次Scan现在只含有少数几个分量,对于编号比较大的的高频(例如:6-63)可能会有大量block完全是0。为了进一步节省字节,在EOB的基础上,我们又重新定义了一组EOBn控制符,来表示之后n个block都是空(纯0)的。当然这些控制符也会被霍夫曼编码了,就像EOB和ZLF一样。

Successive approximation说起来就有点复杂了。如果上面的scan file有仔细看,就会发现每个部分(DC或者每个component的AC)第一次Scan发送的比特数不一,但是之后每次Scan都只输送一个bit。这也是JPEG的规定。对于DC,很容易理解:假设我们某个MCU的某个component是7好了,那么二进制就是111,三位。如果我们分成两次Scan,第一次只传送前两位——也就是11,那么很显然,其“分类单元”应该是0x02;在第二次Scan的时候,因为很显然只有一位,那么分类单元那部分就可以省略掉了,直接把最后的bit补齐即可。

结语

那么,回到最开头的问题,为什么用progressive模式,就会体积减小呢?这里我没有一个确定的答案(…),但是可以看到,和DC/AC全部interleave在一起的Sequential模式相比,其最大的优势是把各个MCU的相似的分量都排在了一起;可以想到,这么做绝对有利于含有预测性质的游程编码,乃至后面的霍夫曼编码。另外,单纯把数据分成好几份(好几个Scan)然后设定不同的霍夫曼表这件事本身可能就能提升不少效率。

 

魔力女管家音乐歌词数据库搭建完成,顺便说说Sphinx

就像上一篇文章许诺的,我费了三天时间,把魔力女管家歌词库给搭出来了!

先上地址:http://fireattack.github.io/mahoromaticdb/ 在Github因为侵权把我的网站日掉之前,暂时就挂在这里了。

包括了所有的CD的简要信息,以及所有歌曲信息歌词(大部分含翻译)。所有的CD我都用当年收藏的Booklet精心制作了1000 px的封面(除了少数几个找不到BK的),欢迎使用。哦里面在最后还随意地包含了两张同人CD(C60、C61发售)的信息,其中第一张网上应该是能找到的,意外地非常好听,强烈推荐。

搭建过程中才发现我之前的文档写的有多烂。有好多后来才找到歌词的歌没包括就算了,格式也是一团糟,错字、标点符号不统一的问题比比皆是。看来我当年的强迫症要轻得多啊!我尽量把标点符号统一为:跟日文用全角,跟英文用半角,但是连用符号(例如:!?)用半角,波浪号的副标题前空一格(但是魔力女管家第二季的标题则不空),括号统一用全角。艺术家名义统一用角色(声优)的格式,但是少数早期CD直接单用声优名字。

呃好吧我承认这并不是特别统一…因为我有个更严重的强迫症,叫做“名从主人”…我一般会尽量遵照BK上的写法,因此牺牲一些统一度。

接下来的地方讲讲搭建网站过程中的一些值得记录的东西吧。

这次用的技术是Sphinx——一个用Python写成的Doc建站软件。之所以没有用Github Pages支持的Jekyll,主要是那个是用Ruby写的,不想去接触。不过后来才发现使用过程中99%都只是在和reStructuredText这玩意打交道,和后端的语言一点关系都没有,早知道就用Jekyll了——毕竟那个是用更简单的Markdown的来生成HTML的。

果然还是先说reStructuredText这种标记语言吧,毕竟大部分时间都耗在和他打交道了。一言以蔽之,这玩意的语法非常的反直觉和不灵活。不过用了三天之后,也算是慢慢熟悉了。reStructuredText的设计思想就是非常注重可读性:基本上而言,源代码就已经在ascii的层面上“格式化”了。例如其最反人类的设定——表格,正常来讲你需要手动用各种线把框框画出来(见下)!

+------------------------+------------+----------+----------+
| Header row, column 1   | Header 2   | Header 3 | Header 4 |
| (header rows optional) |            |          |          |
+========================+============+==========+==========+
| body row 1, column 1   | column 2   | column 3 | column 4 |
+------------------------+------------+----------+----------+
| body row 2             | Cells may span columns.          |
+------------------------+------------+---------------------+
| body row 3             | Cells may  | - Table cells       |
+------------------------+ span rows. | - contain           |
| body row 4             |            | - body elements.    |
+------------------------+------------+---------------------+

还好,还有csv table可以用,否则这真的要死人的(不过这里有个table ganerator可以用)。

除了最基本的一些标记(例如,粗体、斜体啥的,不过注意和Markdown不同),reStructuredText核心元素是directive和role这俩东西。前者是一种特定格式/结构的元素,一般“成块”出现;一般格式是:

.. directivename:: argument ...
   :option: value

   Content of the directive.

在第一行调用directive的名字,然后在第二行起带缩进写设置,然后空一行带缩进(缩进必须和前面保持一致)写被格式化的内容(有些时候则没有内容,比如图片啥的)。这个东西可以用来实现插入目录、图片、目标(锚点)、给内容加class等功能。role和directive类似,但是一般是用于行内(inline)mark一些内容,例如上面提到的粗体、斜体啥的,本质上也是一种预定义好的role。你也可以自定义role。

那么就大致按照我写站的时间顺序来讲吧,没啥逻辑关系。

在建立了你的Sphinx网站之后(推荐使用官方带的Sphinx-quickstart),第一件事在index(链接是到rst源代码,下同)页面中把站点的目录放在上面。这个倒是蛮简单,用Sphinx自带的toctree这个directive就行。在目录里你可以输入想包括的页面名称,以及目录深度。我设想的结构是所有CD信息在一个页面(cdlist.rst),而歌曲因为歌词较长,则每首歌分割为一个单独的页面放到里子目录songs/里。

这里就遇到了第一个问题:如果你用songs/kaerimichi的方式添加页面到toctree中,他会和cdlist都是平级——而不是处于一个Songs的一级目录之下。稍微研究了一下发现要这么做:在songs/子目录下先建立一个index.rst,然后在该rst中再建立一个toctree(下称toctree2),包含所有songs/下的页面。因为toctree总是从当前目录开始查询的,所以对于toctree2,你只需要罗列所有歌曲页面的名字就行,无需加songs/前缀。当然因为歌曲太多了,我们利用:glob:这个参数,就可以用通配符*来匹配所有页面了。回到根目录index的toctree,我们也只需要包含songs就可以了,会自动把toctree2里面的项显示为二级项(结果)。

正式写页面,对于CD list,我需要在最上面附一个(本页面内的)目录。这个用带的contents directive就能轻松做到。但是默认会加一个很多余的根目录节点,用:local:参数去掉它。搞定后,这个目录就会列出所有的section了。加一个section很简单,只要在一行文字下面加一堆“-”、“=”之类的就行。如果要多级section则需要分别套用不同的符号,不过我这里只有一层所以无所谓。一般而言,每个section会自动生成锚点,上面加的目录就能跳转。

因为页面上某些文字需要是小字,而且这些文字混在正文中(inline),我们需要一个role。当你给内容指定了role之后,生成html会自动指定对应的class名,因此配合CSS就可以实现想要的样式。要定义一个role,要先在文档某处(一般是最开头,我不确定在别的地方可不可以)写:

.. role:: smallfont

之后(注意:每个用到:smallfont:的rst都必须重新写一遍这个…),你就能用:smallfont:`your content`来标记你的内容了。不过这里有个限制:这一段代码的前后必须是非“word”的东西,也就是说中间一般得有一个空格。如果你不想要你的普通内容和小字内容之间有空格?需要加反斜杠来消掉空格。即:

your normal content\ :smallfont:`your small font content without space inbetween`

OK,那现在在CSS里写:

.smallfont{
 font-size: 80%;
 color: grey;
}

就行了。不过,怎么把自定义的CSS包含在你生成的网站呢?这里有好几个办法

  1. conf.py中加上
    def setup(app):
        app.add_stylesheet('custom.css')  # may also be an URL

    (你的custom.css应该在_static/目录下)

  2. conf.py中加上(注意这里又变成从根目录起了…下同)
    html_context = {
     'css_files': ['_static/custom.css'],
    }
  3. 先把你模板里的layout.html拷贝到目录下的_templates里(当然,保证你没删掉conf.py里的templates_path = ['_templates']),然后找地方加一行
    {% set css_files = ['_static/custom.css'] %}

    (在SO看到的是

    {% set css_files = css_files + ['_static/custom.css'] %}

    但似乎使用上并没有区别。)

  4. 还是上面的说的layout.html,直接强行加
    {%- block extrahead %}
      		&lt;link rel="stylesheet" href="{{ pathto('_static/custom.css', 1) }}" type="text/css" /&gt;
    {% endblock %}
    
  5. 最暴力的方法,找到你模板的CSS文件,然后修改之;或者修改之后放在_static/里(原因见下面)。

对于我用的alabaster模板,默认的layout.html已经包含了上面的选择4,所以我只需要把custom.css放在_static/下即可。哦这里顺便说句,你放在_static/下的文件默认全部都会复制到你build出来的html里(不管用不用得到),你可以利用这个来覆盖模板里的东西(如上面的5所述)——不过这里注意是覆盖,不是添加。另外,图片之类的resource就别往里放了,否则会复制两遍(因为所有引用过的图片会自动被Sphinx复制在build目录的_images/目录里),占地方。

搞定了“小字体”这个样式之后,在每个歌曲的页面,我还需要引入两种新的样式:日文和中文,分别用来标记不同语种的歌词,从而实现更好的字体显示效果。

这里因为是成块的内容,我们就不用role了,用一个directive:class。如名字所示,其功能和role类似,也是给一块内容标记class。原始的reStructuredText直接用class就行,但是Sphinx是为Python文档开发的,默认把class给定义成一个role了,所以需要改用rst-class

.. rst-class:: ja

	| まなざし そっと ひとつ
	| 誰にもみつからぬように
	| ふんわり時間だけが
	| 流れては消えてく

这里可以看到,argument(双冒号+空格后面的)自然是想要的class名称。因为没有选项(options),所以内容就从第三行(空一行)开始。至于pipe符号(|)这里的目的是强制换行。另外注意,所有的内容必须有一致缩进(具体多少无所谓,反正不会显示成缩进)——缩进结束就退出了这个rst-class了。而相对地,在普通正文中的缩进就是正常的缩进,而且你加几个空格都会如实反映。当然,你还得去你的CSS文件里定义.ja,这里就不赘述。

在写歌曲页面的时候,我遇到一个非常蛋疼的锚点问题。一般而言,锚点在加section/heading时是自动生成的,但是如果你不想开新的section呢?方法是在文档中加入这么一行:

.. _targetname:

至于引用(指向)目标时,Sphinx推荐的用法是用:ref:这个role:即形如:ref:`targetname`这样来引用。但是注意!一般而言target是配合section来用的,所以会自动成section的名字。但是现在我们是在正文中随便添加的,我们必须显式指定他的名字::ref:`Display Name <targetname>`才行。

但是这里有个问题——在Sphinx里,所有的target和ref都是全局的,跨文件的。所以,我每个文件里的target还不能一样,比如如果我每个文件里都有个.. _ja:,用:ref:`Display Name <targetname>`会不知道飞到哪个文件的_ja锚点里去。

研究了半天,发现只能用reStructuredText自带的引用方式——`targetname`_因为这个只适用于本文件,这样即使我每个文件都有个重名的锚点,也只会正确跳转到本文件内的。不过这个有个缺点,不支持Display name和targetname不一样(就是上面带尖括号的用法),所以我的targetname必须就是我想显示的文字。还好,似乎支持中文字符和符号,我用了“[中文]”当锚点名称也没事儿。最终效果

那么最后需要的功能就是给CD曲目列表里的对应歌曲添加链接了。因为是跨文档引用,所以推荐的方法是给每个文档的标题加上个target,然后用:ref:;但是我嫌麻烦,直接用另外一个role,:doc:做。方法基本一样,直接输入:doc:`filename`就行了——显示的文字自动从对应文档的标题提取。不过这里我也并没有直接这么做,因为Sphinx带了个非常好用的role,叫:any::用它可以智能地自动寻找最接近的reference,可以是:ref:,可以是:doc:。在用:any:之前,我们更可以把它指定为“default role”——在conf.py里添加:

default_role = 'any'

这样,当你使用单个撇括起来时(例如:`songs/kaerimichi`),会自动调用:any:这个role。这里,因为这是文档名,又会进而自动调用:doc:

到这里,基本在reStructuredText里遇到的困难都说完了。在Sphinx这边,我对默认的alabaster模板也没怎么改,但是有一点要注意:如果要用这个模板的完整功能,要修改sidebar为

html_sidebars = {
 '**': [
 'about.html',
 'navigation.html',
 'relations.html',
 'searchbox.html',
 'donate.html',
 ]
}

才行,因为模板自带了一些sidebar并没有包括在默认的设置中。模板带的选择在conf.py里修改html_theme_options,基本很好理解,我就改了logo和字体。

Sphinx有一个自带的“basic”模板,还有一些JS和CSS,基本所有的模板都有inherit。但是那个JS(doctools.js)有个问题(Chrome only,我已经汇报到Sphinx dev team),重现方法如下:

  1. 点一个带锚点的地址
  2. 滚动一些(即,你已经不再在锚点的原始位置)
  3. 点一个(非本页内的)链接,跳转到了其他页面
  4. 点“后退键”

正常来讲,点了后退之后,会后退到之前页面的之前位置。但是bug就是,后退之后会强制再读取一次锚点位置,然后跳转过去,而非你之前的阅读位置。你可以在Python 3的官方文档页面重现此bug,因为他用的就是Sphinx。相反Sphinx自己的文档页面则不会,因为他用的是旧版的doctools.js文件。Firefox下无此bug。

我研究了半天,发现是该JS文件中的以下函数:

/**
* workaround a firefox stupidity
* see: https://bugzilla.mozilla.org/show_bug.cgi?id=645075
*/
fixFirefoxAnchorBug : function() {
if (document.location.hash)
window.setTimeout(function() {
document.location.href += '';
}, 10);
},

导致的。讽刺地是,该函数的目的是为了workaround Firefox的一个bug——Firefox对于HTML5新增的<section>锚点tag支持不好——才加的。本来函数有一个判断是只对Firefox有效,但是由于JQuery移除了$.browser,这里被改成了对所有浏览器有效,从而导致了上面所述的副作用。因为那个Firefox的bug对一般应用并没有什么影响(一般应用的锚点都是靠id="xxx"来搞的,并不会用到<section>),所以我们这里直接删掉就好。删除的方式则是复制一份这个JS文件修改,然后放到_static/文件夹内。这样,每次build,会自动覆盖。注意复制的时候,别复制成了原始模板文件夹里那个doctools.js_t了——那个是个JS“模板”,中间有些参数还没生成的。正确的方法是先build一遍,然后从html的目录里复制个原始版的JS。

还有一点要注意,用sphinx-build来build HTML文件的时候,有的时候并不会刷新改刷新的文件(尤其是sidebar之类的),所以隔一段时间最好把_build/目录全删了然后重新build。当然你也可以用-E选项,不过一般还是别强制了,因为要慢许多。

往Github Host的时候,需要加个.nojekyll文件来禁用GitHub Pages自带的翻译引擎,否则会有问题。但是这个文件只有加在根目录才有效,而我是把整个网站放到mahoromaticdb/子目录下的(因为GitHub一个用户好像就支持一个站),所以一开始迷惑了一阵为什么不好使。