Category Archives: Rust Learning

从零开始的 Rust 学习笔记(17) —— 做一个提问箱 Boxy ?

于是我自己的提问箱就是 https://ask.[data deleted]

源代码在这里 #/boxy ?(给大家表演如何用一个项目气死你写前端和 Rust 的朋友(x

回答是人工的,并不是人工智能?(不过感觉加上人工智能的话似乎也蛮好玩诶!

然后因为是模仿 peing 或者 sarahah 那样的匿名的提问箱,所以这个截图里就差不多是后台了~既然说是匿名的话,那就是真匿名~除了记录了提问时间,IP 只用来做了速度限制,没扔数据库。Question 在 MongoDB 里的模型则是

{ 
    "_id" : ObjectId("5e0a0ca0001cd1ea00876f2e"), 
    "question" : "What's this?", 
    "question_time" : NumberLong(1577716896), 
    "answer" : "It's a question box!", 
    "answer_time" : NumberLong(1577716906), 
    "id" : NumberLong(0) 
}

箱子的 owner 登录之后,直接点击答案的部分就进入编辑 / 回答模式,然后也可以删除提问什么的~

代码里倒是 Web 和纯 API 方式都实现了~然后回答之后自动生成 Twitter 卡片什么的还没有做_(:3」∠)_

再从技术层面上来说的话,这个项目用 Rust 上的 Hyper 作为 HTTP 服务器,然后数据库使用了 MongoDB,考虑到是作为单一用户的提问箱,因此用户名和密码是需要写在 boxy.json 里的~当然放进数据库里的是密码加盐后再 HMAC + SHA512 过的

stored_password = HMACSHA512(password + password_salt, password_salt)
{
    "_id" : ObjectId("5e09a250004b7da70096e3dc"),
    "user" : "ryza",
    "password" : "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}

Web 界面和后端做了分离,我自己写了一个简单的~因此就直接手写了,没有用到现在前端流行的 Vue 或者 Angular 之类的框架。访问后端的 API 也是直接用的 jQuery 的 AJAX 模块。

登录之后会返回一个 token,包含了有效期,默认是 365 天,保存在了浏览器的 localStorage 里。登出的时候会自动清掉,但不是关闭页面 / 浏览器自动清除,所以在公用电脑上要记得登出~

那么下面是目前有的一些 API~ 以 http://localhost:5534 为例子!

Continue reading 从零开始的 Rust 学习笔记(17) —— 做一个提问箱 Boxy ?

从零开始的 Rust 学习笔记(16) —— K-means 模版

重构一下前两天用 Rust 写的 Colorline 中 K-means 聚类算法的部分~因为之前 kmeans 放在了 dominant_color.rs 下,显然 kmeans 这个算法不应该属于 dominant_color;同时,之前的 kmeans 算法只能用在这里,考虑到以后代码复用的话,当然是要写成模版啦╮(╯▽╰)╭

kmeans 独立出来之后,让 kmeans 可以接受任意 impl 了 KmeansComputable trait 的类。其实很久以前用 C++ 也写了一个比较通用的 K-means 模版,但是当时并没有考虑 trait 这样的,而是直接用了两个回调函数(不过写完这个 Rust 版本的之后似乎突然有点思路了)

Rust 这个写起来思路很清晰,首先就是 kmeans 函数应该接受:

  1. 一组待聚类的数据 array
  2. 要求聚类的的类数 k
  3. 收敛条件 min_diff —— k 个类每次迭代后各类中心点移动距离的上界

其中,array 应该是 KmeansComputable 的。

那么 KmeansComputable 这个 trait 的设计的话,第一点显然是要可以给出该类任意两个 instance 之间的距离;第二点则是可以在给出一组该类的后计算其中心点。也就是

pub trait KmeansComputable {
    fn distance(&self, other: &Self) -> f64;
    fn compute_center(cluster: &Vec<Self>) -> Self where Self: Sized;
}

于是 kmeans 函数如下~(高亮的部分则是用到 KmeansComputable trait 里要求实现的函数的地方)

Continue reading 从零开始的 Rust 学习笔记(16) —— K-means 模版

从零开始的 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) —— 计算图像相似度

从零开始的 Rust 学习笔记(13) —— YouTube Playlist Watcher & Downloader

最近在 YouTube 上听了很多 Harutya 的作品,然后也看了各种 Talk 的视频。在电脑上看的时候,对感兴趣的视频可以很方便的用 Python 的 youtube-dl 直接下下来,但是在 iPad 或者手机上的时候就不那么方便了。

  1. 整体思路
  2. 创建公开的 YouTube Playlist
  3. Playlist Watcher Config 配置文件
  4. Rust 代码模块组织
  5. PlaylistWatcherConfig
  6. YouTubeError
  7. Decode Percentage Encoded URL
  8. Access YouTube API
    1. HTTP Client
    2. Get All Video ID in Playlist
    3. Get Video Info
    4. Code of src/youtube/api.rs
  9. Stream
    1. Stream utility functions
    2. DownloadableStream Trait
    3. Code of src/youtube/stream.rs
  10. Video
    1. VideoStream
    2. AudioStream
    3. Video 类
  11. Playlist
  12. Downloader
  13. Watch Playlist and Download
    1. VideoConsumerMessage
    2. VideoConsumer
    3. PlaylistWatcher
    4. Code of src/youtube/video_consumer.rs
    5. Code of src/youtube/playlist_watcher.rs
  14. YouTube module / crate
  15. YouTube Playlist Watcher & Downloader
  16. 后记

项目的源代码同时也在我的 GitHub 上~ #/watchyou

1. 整体思路

那么想了一个 workaround ——

先在 YouTube 上创建一个公开的 Playlist,然后看到有想要下载的视频之后,就把它放进这个 Playlist 里面。假设这个 Playlist 的 URL 是

https://www.youtube.com/playlist?list=PLmPVZgHRcD6ZzbLAxHcP5FJRUOsUP5g4G

接下来,用代码每隔一段时间抓取一次这个 Playlist 的网页,然后 parse 出在这个 Playlist 里的所有 Video 的 ID。

下一步的话,就是用 YouTube 的 API 去获取每个 Video ID 所对应的视频的信息。

最后 parse 出来 API 返回数据里面的每一个 Video 的音频流和视频流的下载地址,并且下载即可~

2. 创建公开的 YouTube Playlist

比如我们这里创建一个名为 save2disk 的公开 Playlist

然后就可以拿到这个 Playlist 对应的 URL 了~

这里的的 URL 是

https://www.youtube.com/playlist?list=PLmPVZgHRcD6ZzbLAxHcP5FJRUOsUP5g4G
Continue reading 从零开始的 Rust 学习笔记(13) —— YouTube Playlist Watcher & Downloader

从零开始的 Rust 学习笔记(12) —— 用 Rust CLI 来飞 Tello Drone

试手了一下 DJI 和 Intel 出的 Tello 迷你四轴飞行器~

Tello 起飞之后基本上还是比较稳,但是在相对较小的室内还是会受到自身气流的干扰,在室外有风的时候也能看到 Tello 随风摆动。

在玩了一段时间之后,感觉 Tello,或者说所有四轴飞行器的硬伤还是电池。Tello 标准套装里只有 1 块电池,满电续航差不多10分钟,说实话这个时间真的还蛮短的。我的 Kit 里包含了 3 块电池,理论上「车轮战」的话似乎可行,但除非自己一直跟着飞行器,要不然实际可以飞的距离还是受限于单块电池。

此外,Kit 里包含的充电器虽然可以同时插 3 块电池,但其并不能同时为 3 块电池充电。因此就算是「车轮战」可能也坚持不了多少,带 3 块电池车轮战的话,最多玩 1 小时左右吧。

主要的缺点说完之后,来说说 Tello 比较好玩的地方。首先是有 SDK,可以自己编程上去,要想更灵活的话,也可以自己按照 API 来写 —— 也就是这篇 post 玩的,当然并没有实现全部功能。其次是非常轻,3 块电池加上飞行器本体背在包里几乎没有什么感觉。

先放几张拆箱图吧(^O^)

这个小盒子里面装的就是 Tello 了(((o(*゚▽゚*)o)))

在说明书下面还有一套备用替换的螺旋桨~♪(´ε` )

Continue reading 从零开始的 Rust 学习笔记(12) —— 用 Rust CLI 来飞 Tello Drone

从零开始的 Rust 学习笔记(11) —— 让 Breezin 用上 RESTful API 和 Access Token

於是接著上一篇 Rust 學習筆記,讓上次寫的 Breezin 用上 RESTful API 和 Access Token~之前的 HTTP API 的話就是特別樸素的那種,比如

http://10.0.1.2:2275/get?name=fan1
http://10.0.1.2:2275/set?name=fan1&value=2000
http://10.0.1.2:2275/set?name=fan1&value=auto

並且上面的都是 GET 請求,好處就是在瀏覽器裡手動輸入相應的 API 和引數就能呼叫;壞處就是非常不 RESTful,表示是否成功的狀態碼只在返回的 JSON 中,而 HTTP 的狀態碼都是 HTTP 200 OK;其次,動詞 setget 都在 URL 中出現,而不是像 RESTful API 規範的那樣,體現在 HTTP Method 上。

使用 RESTful API 的話,我們的請求就是如下樣子的了~

请求数据

HTTP MethodAPI EndpointDescription
GEThttp://10.0.1.2:2275/api/v1/fansGet all fans status
GEThttp://10.0.1.2:2275/api/v1/fans/:idGet fan status of given :id
GEThttp://10.0.1.2:2275/api/v1/tempsGet all smc temperature sensors' status
PUThttp://10.0.1.2:2275/api/v1/fans/:idUpdate specified property of fan with :id

當然,更新風扇的屬性的話,實際上可寫入的就只有 3 個 —— min, manualoutput。那麼要傳值的話,肯定就是放在 PUT 方法的 body 裡面了~

例如需要設定 fan1 的最低 RPM 為 2000 的話,那麼就使用 PUT 方法訪問的 API Endpoint 是 http://10.0.1.2:2275/api/v1/fans/1,其 body 為

{
  "property": "min",
  "value": 2000
}

同時,因為選擇哪一個風扇是在 URI 上確定的,因此也需要用一下正則表達式去匹配。這裡我們用到的正則表達式如下~

Continue reading 从零开始的 Rust 学习笔记(11) —— 让 Breezin 用上 RESTful API 和 Access Token

从零开始的 Rust 学习笔记(10) —— Breezin

家裡有一臺半閒置的 12 年的 Mac Mini,之前偶爾跑點 Docker 的東西,順便還有把一臺超舊的印表機共享到局域網裡。不過想想應該拿它做點別的事,比如裝個 Linux 然後搭上 v2ray 做軟路由實現透明代理,然後再裝個 Docker 偶爾測試自己寫的 Linux 的東西~

不過這一篇 post 並不是寫如何用 v2ray 在 Linux 上搭軟路由,而是想起現在的軟路由用的是 Raspberry Pi 4。Raspberry Pi 4 在實際使用的時候還是不錯的,但是發熱量比較大,以及 softirq 看起來略有點爆炸 233333

因為打算用 Raspberry Pi 4 做點別的專案,那麼軟路由透明代理的 workload 就交給 Mac Mini 好啦,再寫個程式手動控制一下 Mac Mini 的風扇轉速~這樣就不會因為 workload 比較大,然後晚上風扇轉速太高影響睡眠? 白天的時候倒是基本無所謂。測試的時候發現 3000-3300 RPM 幾乎聽不到聲音,同時也比最低速 1800 RPM 高出一截,不太會因為過熱而出現問題~(╹ڡ╹)

那麼名字就叫 Breezin 好啦(一邊聽彩彩的 Breezin' 一邊寫~

當然,僅僅說完成功能的話,Shell Script 都完全足夠,但是既然是正好在玩 Rust 的話,那就用 Rust 寫來玩玩吧(*^3^)

在 Mac Mini 上安裝了 Ubuntu 18.04 LTS 之後,SMC 報告的風扇和溫度等資訊都被對映在了 /sys/devices/platform/applesmc.768

➜ ~ ls /sys/devices/platform/applesmc.768/fan*
-r--r--r-- 1 root root 4096 12  3 14:52 /sys/devices/platform/applesmc.768/fan1_input
-r--r--r-- 1 root root 4096 12  3 14:52 /sys/devices/platform/applesmc.768/fan1_label
-rw-r--r-- 1 root root 4096 12  3 14:53 /sys/devices/platform/applesmc.768/fan1_manual
-r--r--r-- 1 root root 4096 12  3 14:52 /sys/devices/platform/applesmc.768/fan1_max
-rw-r--r-- 1 root root 4096 12  3 14:54 /sys/devices/platform/applesmc.768/fan1_min
-rw-r--r-- 1 root root 4096 12  3 14:53 /sys/devices/platform/applesmc.768/fan1_output
-r--r--r-- 1 root root 4096 12  3 14:52 /sys/devices/platform/applesmc.768/fan1_safe

這裡可以看到實際可寫入的只有 fan1_manual, fan1_minfan1_output

fan1_label 裡面儲存了風扇的名字。fan1_manual 實際上會被解釋成一個 Boolean 值,0 代表系統控制,1 代表手動設定。

fan1_min 是對應風扇的最低的 RPM,這個是我們可以控制的。相應的 fan1_max 則是最大轉速,但是不能限制風扇的最大轉速。

fan1_input 是表示當前風扇報告的 RPM,但是是一個只讀的量。fan1_output 表示需要對應風扇達到的 RPM,當 fan1_manual 的值為 1 時有效,否則寫入之後也會被系統覆蓋(即自動控制轉速)。

最後的 fan1_safe 看起來大概是該風扇的最低?安全轉速,然而雖然寫了可讀,實際測試的時候並不可讀(也許是 Ubuntu 下 SMC 驅動的問題?)。

➜ ~ cat /sys/devices/platform/applesmc.768/fan1_safe
cat: /sys/devices/platform/applesmc.768/fan1_safe: Invalid argument

在知道了這些對應的對映之後,想法就是在使用者請求資訊的時候,去 glob /sys/devices/platform/applesmc.768/fan*,然後做成 JSON 資料返回。在使用者設定的風扇轉速的時候,就寫入到對應的 output 裡面,並將 manual 設定為 1

於是設想就是做一個 HTTP API,假如 IP 是 10.0.1.2,服務執行在 2275 埠的話,那麼要做的就是如下 2 個 API

Continue reading 从零开始的 Rust 学习笔记(10) —— Breezin

从零开始的 Rust 学习笔记(9)

Well, at this point, The Rust Programming Language demonstrates how to write a command line program, which named minigrep. Followed the textbook, I decided to rewrite the small utility that I mentioned in https://ryza.moe/2019/08/rewrite-the-styled-code-in-html-generated-by-apple-to-wordpress-compatible-html/.

The things learnt so far is enough to support me to write a, at least, workable utility. And if you're an expert in Rust, you'll find the following code is ugly and perhaps even not Rust-ish.

However, based on the previous 9 posts of this series, for these who just begins to learn Rust lang like me, the code which will be shown below won't be a giant jump. Nevertheless, there definitely has plenty of room to improve the following code. Any suggestions or questions are welcomed(⁎⁍̴̛ᴗ⁍̴̛⁎)

Furthermore, I googled a lot during writing the code. So I also attached corresponding link in comments.

Continue reading 从零开始的 Rust 学习笔记(9)