Category Archives: OpenCV

从零开始的 Rust 学习笔记(15) —— Colorline

在圣诞节的时候一个人回顾了一下 LoveLive μ's 3rd Live,然后就做了这个~从视频里提取每个时间点的主要色彩并生成一张大的图。

假如视频时长为 01:43:25,每隔 1 秒计算一次其画面的主要色彩,并且在画出高度为 120 像素,宽度为 1 像素的 colorline 的话,就可以组成下面这样图的啦~

那么理论上长度就是 103 * 60 + 25 = 6205 秒,也就是 6205 像素。但是实际上需要注意的是 FPS 的获取,视频时长的计算和如何选择帧。因为视频的 FPS 可能并不是一个整数,而是类似 29.97 这样的浮点数,可是在 OpenCV 里并不支持按秒读取。于是只好先获取视频的 FPS 与 FRAME_COUNT。

但是如果直接 as i32 的话,原本 29.97 的 FPS 就会变成 29,那么显然对视频时长的计算就会出错,6205 * 29.97 / 29 = 6412 秒,足足多了 207 秒!(那么问题来了)同时也会导致生存的图片在时间轴上不够精确。因此在代码上需要注意一下~

那比如在 3135 像素附近,开始出现了很多蓝色的条~粗略算一下也就是视频的 52:15 附近,这个显然就是「賢い、かわいい —— エリーチカ!」

(下面两张截图是在对应的区段里随便找的~并不是严格对应到起点的)

然后到了 3310 像素附近是大段连续紫色出现的起点~也就是视频里 55:10 的地方~「希パワーたーっぷり注入 はーいっプシュ!

那么实现起来的话,也并不算是很复杂~项目的源代码在 GitHub 上~#/colorline

1. 整体思路

首先需要视频文件的路径,然后用户要求每隔多少秒计算一次画面的主要色彩。接下来打开文件之后,计算视频以秒为单位的长度以及按照要求的话,需要产生多少根 Colorline。

随后启动一个抽取视频每一帧的线程,Video Extraction Thread。因为视频并不能真正的随机访问某一帧(可参考视频编码原理, P 帧、I 帧的概念等),故将会顺序遍历一次所有的 frame。

接下来到了应该被抽取出来计算主要彩色的帧的时候,为了利用好 CPU 资源,肯定会放到别的线程上去计算,不会放在 Video Extraction Thread 里做 。但是每到一个都另起一个线程的话,显然一会儿线程的数量可能就爆炸了~

那么我们就用一个线程池来做。这个线程池里的每一个 worker 接收的参数是一个需要计算的画面,以及这个画面对应的 Colorline 的 index。一个 worker 一次只负责计算某一帧的主要色彩,然后将计算结果与对应的 Colorline 的 index用 channel 送回。

为什么不直接画在 Mat 上?理论上这些线程访问的内存资源都不会有冲突,但是 Rust 里 OpenCV bindings 的 Mat 是不能在线程之间共享的(要么就一路 unsafe 走起,但那样似乎不如直接用 C++ 写了)。

所以对于线程池 worker threads 的计算结果,我们需要另外 spawn 一个 Worker-Thread Gather Thread,它负责收集所有 worker 计算的结果。如何判断收集完呢?我们已经有了视频长度与用户希望的间隔时间,那么提前就可以计算出一共会从 channel 中收到多少 message。

最后,用户可以 poll 我们的计算结果。如果已经计算完成,但是还没生成最后的图,那么就生成好 Mat 然后返回;如果还没好的话,就告知 InProgress;要是中间什么环节出错的话,就返回 Err

好了,基本的想法确定下来了,下面就可以开始写大坑了(*^3^)

Continue reading 从零开始的 Rust 学习笔记(15) —— Colorline

从零开始的 Rust 学习笔记(14) —— 计算图像相似度

上一篇 post 记录了 OpenCV 的编译脚本,当然是因为马上会用到啦~在 Rust 中要用 OpenCV 的话,则是需要用到 twistedfall/opencv-rust 这个 binding。使用方法其实倒也蛮简单,只不过 Rust 里面的函数没有参数默认值这个东西,于是在Rust 里使用 OpenCV 函数的时候,还能顺便记忆 OpenCV 里函数都有哪些参数╮( ̄▽ ̄"")╭

在有了图像相似度之后,就可以做比如查找某一目录下是否有重复的图片之类的,或者给出一张图,查找某个目录下与它最相似的图片等等~

计算方法

当然话说回来,计算图像相似度本身是有很多种方法的,这里因为学习 Rust 为主要目的,于是暂且不在计算方法上做什么创新。后文中使用到的计算图像相似度的算法与参数取自 MoeOverflow 组织 @Shincurry 的 AnimeLoop 项目,详细的参数选择解释可以在 Shincurry 的博客里找到~ https://blog.windisco.com/animeloop-paper/

简单来说,我们会将图片转化为灰度图,然后缩放到 64x64 的大小,并转换其底层的数据类型为 f64,随后计算其相应的离散余弦变换「Discrate Cosine Transform」。

接着取离散余弦变换结果矩阵的左上 $16\times 16$ 的子矩阵,计算其均值,需要注意的是,$(0, 0)$ 的值要排除在外。因为 $(0, 0)$ 是其直流分量「DC coefficient」,如果用来计算平均值的话,则可能会明显影响计算结果。那么在计算平均值的时候的总个数就是 $16 \times 16 - 1$。

在有了左上角 $16 \times 16$ 矩阵的均值 $m$ 之后,就可以依次将这个 $16 \times 16$ 矩阵的每一个元素 $v_{(p, q)}$ 与 $m$ 相比较,如果 $v_{(p, q)} \lt m$,那么 pHash 字符串就最末尾增加 "1";否则则增加 "0"

在有了两张图片的 pHash 字符串 $\mathcal{A}, \mathcal{B}$ 之后,我们计算两个 pHash 的汉明距离「Hamming Distance」 $d$,然后相似度则为 $r = 1.0 - \frac{d}{l}$,其中 $l$ 为 pHash 字符串长度。

\begin{align} &d = 0\\ &l = 16\times 16\\ & \forall i \in [0, 16\times 16)\\ & \left\{ \begin{aligned} d = d + 1, &\, \mathcal{A}_i \lt \mathcal{B}_i\\ d = d + 0, &\, \mathcal{A}_i \ge \mathcal{B}_i \end{aligned} \right.\\ &r = 1.0 - \frac{d}{l} \end{align} Continue reading 从零开始的 Rust 学习笔记(14) —— 计算图像相似度

Ryza 的编译安装 OpenCV 的 script

嘛,就当是记录一下好了,只需要知道自己要安装的 OpenCV 的版本号就行~

顺便处理了一下 OpenCV 4 之后生成 pkgconfig 文件「opencv4.pc」的小坑,

OpenCV 3 和 4 都没问题,2 的话没有测试过;然后 Python binding 是 Python 3,因为 Python 2 本身也快 deprecated 了。最后就是需要有 cmake, curl, unzip 和 pip。当然 freetypeharfbuzz 包也是需要的,用对应的包管理软件安装一下即可~

于是下面的代码需要根据自己需求改的部分就是一个 OpenCV 的版本号,然后是跟生成 pkgconfig 文件有关的那两行,OpenCV 3 应该不用的,OpenCV 4 则需要加上。

export OPENCV_VER=4.4.0

export OPENCV_SRC="https://github.com/opencv/opencv/archive/${OPENCV_VER}.zip"
export OPENCV_CONTRIB_SRC="https://github.com/opencv/opencv_contrib/archive/${OPENCV_VER}.zip"

curl -L -o "opencv-${OPENCV_VER}.zip" "$OPENCV_SRC"
curl -L -o "opencv_contrib-${OPENCV_VER}.zip" "$OPENCV_CONTRIB_SRC"

unzip "opencv-${OPENCV_VER}.zip" 1>&2 > /dev/null
unzip "opencv_contrib-${OPENCV_VER}.zip" 1>&2 > /dev/null

export OPENCV="`pwd`/opencv-${OPENCV_VER}"
export OPENCV_CONTRIB="`pwd`/opencv_contrib-${OPENCV_VER}/modules"

pip3 install numpy

mkdir -p "${OPENCV}/build"
cd "${OPENCV}/build"
cmake -D CMAKE_BUILD_TYPE=RELEASE \
    -D CMAKE_INSTALL_PREFIX=/usr/local \
    -D OPENCV_EXTRA_MODULES_PATH="${OPENCV_CONTRIB}" \
    -D PYTHON3_LIBRARY=`python3 -c 'import subprocess ; import sys ; s = subprocess.check_output("python3-config --configdir", shell=True).decode("utf-8").strip() ; (M, m) = sys.version_info[:2] ; print("{}/libpython{}.{}.dylib".format(s, M, m))'` \
    -D PYTHON3_INCLUDE_DIR=`python3 -c 'import distutils.sysconfig as s; print(s.get_python_inc())'` \
    -D PYTHON3_EXECUTABLE=`which python3` \
    -D BUILD_opencv_python2=OFF \
    -D BUILD_opencv_python3=ON \
    -D INSTALL_PYTHON_EXAMPLES=OFF \
    -D INSTALL_C_EXAMPLES=OFF \
    -D OPENCV_ENABLE_NONFREE=ON \
    -D OPENCV_GENERATE_PKGCONFIG=ON \
    -D OPENCV_PC_FILE_NAME=opencv4.pc \
    -D BUILD_EXAMPLES=OFF ..

等生成好 Unix makefile 之后就可以开始编译了~macOS 上则是

make -j`sysctl -a | grep 'hw.ncpu' | awk '{print \\$2}'`

*nix 则是

make -j`nproc`

最后安装即可~

sudo make install

A Simple Approach to Add Invisible Watermark with OpenCV

摸鱼摸鱼,今天试试用简单的频域隐写水印~

其实隐写水印的方法也有不少了,这里是其中一个简单的方法,使用的是离散傅立叶变换,相比起小波变换的版本,这里的鲁棒性没有那么强,但也还是挺好玩的,下次有时间可以试试看小波变换的方法233333

在频域增加水印的好处是肉眼不易看见,而且对于一般的裁剪、拉伸、涂抹有较强的抵抗性~比如下面的图像就加上隐写的水印,但是与左侧的原图几乎没有视觉上的差异。

在将两张图转换到频域上之后,则一眼能看到右侧图上的水印~

Continue reading A Simple Approach to Add Invisible Watermark with OpenCV

Magic Image(3)——Implementation in Python3 with Either OpenCV3 or PIL

It has been 3 years since the last update on Magic Image, https://await.moe/2016/09/magic-image2-mathematical-model/, which talked about the mathematical model of creating the mix image.

And to be honest, the Python implementation actually wrote 4 months ago, but it only support OpenCV then. And today, out of personal interest, I added PIL support. Now it could run with PIL only. (But if it detects the existence of OpenCV, that would be preferred)

Continue reading Magic Image(3)——Implementation in Python3 with Either OpenCV3 or PIL

「Single Image Haze Removal Using Dark Channel Prior」Python Implementation with OpenCV

Haze Removed via Dark Channel Prior
Haze Removed via Dark Channel Prior

A Brief Review


Single Image Haze Removal Using Dark Channel Prior」是 2009 年 CVPR 最佳论文,何凯明博士在这篇论文中提出了暗通道先验的图像去雾算法。

那么暗通道先验是什么呢?这种方法是基于这样的一个观察:

It is based on a key observation most local patches in haze-free outdoor images contain some pixels which have very low intensities in at least one color channel.

简单来说就是对绝大多数没有雾的图像来说,它们的一些局部区域的像素中,某些像素至少有一个颜色通道的值很低。或者对应于原文的「low intensities」,即光强度很低。

我们需要对图像去雾的原因主要是图像中的雾会给很多算法带来麻烦,比如物体识别,特征提取等,它们都默认输入的图像是清晰的,没有雾的。或者不考虑算法,对于在野外或者无人机上的监视摄像头,遇到有雾的场景也是经常的事,即使是人工监视也是需要去雾的。

顺便一提,利用暗通道先验的算法去雾,还可以得到不错的景深图。

Last, the haze removal can produce depth information and benefit many vision algorithms and advanced image editing. Haze or fog can be a useful depth clue for scene understanding. The bad haze image can be put to good use.

在有雾的图像中,一个广泛使用的成像数学模型如下

\begin{equation}
    \mathbf{I}(\mathbf{x})=\mathbf{J}(\mathbf{x})t(\mathbf{x})+\mathbf{A}(1-t(\mathbf{x}))\tag{1}
\end{equation}

我们可以简单的将$\mathbf{x}$理解为图像中的某一个位置,那么,$\mathbf{I}(\mathbf{x})$则是我们最终观察到的有雾的图像在该点的强度;$\mathbf{J}(\mathbf{x})$是在没有雾的情况下,该点应有的强度;$t(\mathbf{x})$是该点的透射率(the medium transmission describing the portion of the light that is not scat- tered and reaches the camera);最后,$\mathbf{A}$是全局大气光强(global atmospheric light)。

图像去雾的目标就是从一张有雾的图像$\mathbf{I}(\mathbf{x})$中,恢复出没有雾的图像$\mathbf{J}(\mathbf{x})$,透射率$t(\mathbf{x})$以及$\mathbf{A}$,全局大气光强(global atmospheric light)。

其中,我们又把$\mathbf{J}(\mathbf{x})t(\mathbf{x})$的结果叫做「直接衰减,direct attenuation」,这个应该比较好理解,就是原始位置反射的光,经过介质(如雾、空气中的其他颗粒)时发生的衰减。然后我们又把$\mathbf{A}(1-t(\mathbf{x}))$叫做Airlight,也就是(先前经过介质时的)散射光导致的色偏。

airlight-and-direct-attenuation

Continue reading 「Single Image Haze Removal Using Dark Channel Prior」Python Implementation with OpenCV