1
0
Fork 0
mirror of synced 2024-09-28 15:31:25 +12:00

Merge pull request #1 from nagadomi/master

Update branch
This commit is contained in:
Álex G.M 2016-02-11 10:54:04 +01:00
commit cb2d4386c2
94 changed files with 3564 additions and 1065 deletions

2
.gitattributes vendored Normal file
View file

@ -0,0 +1,2 @@
models/*/*.json binary
*.t7 binary

14
.gitignore vendored
View file

@ -1,4 +1,16 @@
*~ *~
work/
cache/*.png cache/*.png
models/*.png cache/url_*
data/*
!data/.gitkeep
models/*
!models/anime_style_art
!models/anime_style_art_rgb
!models/ukbench
!models/photo
models/*/*.png
waifu2x.log waifu2x.log
waifu2x-*.log

1
NOTICE
View file

@ -1 +1,2 @@
images/miku_*: CC BY-NC by piapro (http://piapro.net/en_for_creators.html) images/miku_*: CC BY-NC by piapro (http://piapro.net/en_for_creators.html)
webgen/assets/bg.png: Generated by chibichara maker (http://tetrabo.com/chibichara/).

View file

@ -1,6 +1,7 @@
# waifu2x # waifu2x
Image Super-Resolution for anime-style-art using Deep Convolutional Neural Networks. Image Super-Resolution for Anime-style art using Deep Convolutional Neural Networks.
And it supports photo.
Demo-Application can be found at http://waifu2x.udp.jp/ . Demo-Application can be found at http://waifu2x.udp.jp/ .
@ -19,16 +20,11 @@ waifu2x is inspired by SRCNN [1]. 2D character picture (HatsuneMiku) is licensed
## Public AMI ## Public AMI
``` ```
AMI ID: ami-0be01e4f TODO
AMI NAME: waifu2x-server
Instance Type: g2.2xlarge
Region: US West (N.California)
OS: Ubuntu 14.04
User: ubuntu
Created at: 2015-08-12
``` ```
## Third Party Software ## Third Party Software
[Third-Party](https://github.com/nagadomi/waifu2x/wiki/Third-Party) [Third-Party](https://github.com/nagadomi/waifu2x/wiki/Third-Party)
## Dependencies ## Dependencies
@ -37,10 +33,12 @@ Created at: 2015-08-12
- NVIDIA GPU - NVIDIA GPU
### Platform ### Platform
- [Torch7](http://torch.ch/) - [Torch7](http://torch.ch/)
- [NVIDIA CUDA](https://developer.nvidia.com/cuda-toolkit) - [NVIDIA CUDA](https://developer.nvidia.com/cuda-toolkit)
### lualocks packages (excludes torch7's default packages) ### LuaRocks packages (excludes torch7's default packages)
- lua-csnappy
- md5 - md5
- uuid - uuid
- [turbo](https://github.com/kernelsauce/turbo) - [turbo](https://github.com/kernelsauce/turbo)
@ -57,34 +55,44 @@ See: [NVIDIA CUDA Getting Started Guide for Linux](http://docs.nvidia.com/cuda/c
Download [CUDA](http://developer.nvidia.com/cuda-downloads) Download [CUDA](http://developer.nvidia.com/cuda-downloads)
``` ```
sudo dpkg -i cuda-repo-ubuntu1404_7.0-28_amd64.deb sudo dpkg -i cuda-repo-ubuntu1404_7.5-18_amd64.deb
sudo apt-get update sudo apt-get update
sudo apt-get install cuda sudo apt-get install cuda
``` ```
#### Install Package
```
sudo apt-get install libsnappy-dev
```
#### Install Torch7 #### Install Torch7
See: [Getting started with Torch](http://torch.ch/docs/getting-started.html) See: [Getting started with Torch](http://torch.ch/docs/getting-started.html)
And install luarocks packages.
```
luarocks install graphicsmagick # upgrade
luarocks install lua-csnappy
luarocks install md5
luarocks install uuid
PREFIX=$HOME/torch/install luarocks install turbo # if you need to use web application
```
#### Getting waifu2x
```
git clone --depth 1 https://github.com/nagadomi/waifu2x.git
```
#### Validation #### Validation
Test the waifu2x command line tool. Testing the waifu2x command line tool.
``` ```
th waifu2x.lua th waifu2x.lua
``` ```
### Setting Up the Web Application Environment (if you needed)
#### Install packages
```
luarocks install md5
luarocks install uuid
PREFIX=$HOME/torch/install luarocks install turbo
```
## Web Application ## Web Application
Run.
``` ```
th web.lua th web.lua
``` ```
@ -114,11 +122,20 @@ th waifu2x.lua -m noise_scale -noise_level 1 -i input_image.png -o output_image.
th waifu2x.lua -m noise_scale -noise_level 2 -i input_image.png -o output_image.png th waifu2x.lua -m noise_scale -noise_level 2 -i input_image.png -o output_image.png
``` ```
See also `images/gen.sh`. See also `th waifu2x.lua -h`.
### Using photo model
Please add `-model_dir models/photo` to command line option, if you want to use photo model.
For example,
```
th waifu2x.lua -model_dir models/photo -m scale -i input_image.png -o output_image.png
```
### Video Encoding ### Video Encoding
\* `avconv` is `ffmpeg` on Ubuntu 14.04. \* `avconv` is alias of `ffmpeg` on Ubuntu 14.04.
Extracting images and audio from a video. (range: 00:09:00 ~ 00:12:00) Extracting images and audio from a video. (range: 00:09:00 ~ 00:12:00)
``` ```
@ -144,6 +161,7 @@ avconv -f image2 -r 24 -i new_frames/%d.png -i audio.mp3 -r 24 -vcodec libx264 -
``` ```
## Training Your Own Model ## Training Your Own Model
Notes: If you have cuDNN library, you can use cudnn kernel with `-backend cudnn` option. And you can convert trained cudnn model to cunn model with `tools/cudnn2cunn.lua`.
### Data Preparation ### Data Preparation
@ -151,7 +169,7 @@ Genrating a file list.
``` ```
find /path/to/image/dir -name "*.png" > data/image_list.txt find /path/to/image/dir -name "*.png" > data/image_list.txt
``` ```
(You should use PNG! In my case, waifu2x is trained with 3000 high-resolution-noise-free-PNG images.) You should use noise free images. In my case, waifu2x is trained with 6000 high-resolution-noise-free-PNG images.
Converting training data. Converting training data.
``` ```

View file

@ -3,7 +3,15 @@ require 'pl'
CACHE_DIR="cache" CACHE_DIR="cache"
TTL = 3600 * 24 TTL = 3600 * 24
local files = dir.getfiles(CACHE_DIR, "*.png") local files = {}
local image_cache = dir.getfiles(CACHE_DIR, "*.png")
local url_cache = dir.getfiles(CACHE_DIR, "url_*")
for i = 1, #image_cache do
table.insert(files, image_cache[i])
end
for i = 1, #url_cache do
table.insert(files, url_cache[i])
end
local now = os.time() local now = os.time()
for i, f in pairs(files) do for i, f in pairs(files) do
if now - path.getmtime(f) > TTL then if now - path.getmtime(f) > TTL then

BIN
assets/bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

BIN
assets/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -1,90 +1,146 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html lang="en">
<!-- This file was automatically generated by webgen/gen.rb. Do not make changes to this file manually. -->
<head> <head>
<meta charset="UTF-8"> <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<link rel="canonical" href="http://waifu2x.udp.jp/"> <meta charset="utf-8">
<link rel="shortcut icon" href="favicon.ico"/>
<meta name="viewport" content="initial-scale=1.0,width=device-width">
<link href="//cdnjs.cloudflare.com/ajax/libs/normalize/3.0.3/normalize.min.css" rel="stylesheet" type="text/css">
<link href="style.css" rel="stylesheet" type="text/css">
<link href="mobile.css" rel="stylesheet" type="text/css" media="screen and (max-width: 768px) and (min-width: 0px)">
<script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/jquery-cookie/1.4.1/jquery.cookie.js"></script>
<script type="text/javascript" src="ui.js"></script>
<title>waifu2x</title> <title>waifu2x</title>
<style type="text/css">
body {
margin: 1em 2em 1em 2em;
background: LightGray;
width: 640px;
}
fieldset {
margin-top: 1em;
margin-bottom: 1em;
}
.about {
position: relative;
display: inline-block;
font-size: 0.9em;
padding: 1em 5px 0.2em 0;
}
.help {
font-size: 0.85em;
margin: 1em 0 0 0;
}
</style>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
<script type="text/javascript">
function clear_file() {
var new_file = $("#file").clone();
new_file.change(clear_url);
$("#file").replaceWith(new_file);
}
function clear_url() {
$("#url").val("")
}
$(function (){
$("#url").change(clear_file);
$("#file").change(clear_url);
})
</script>
</head> </head>
<body> <body>
<h1>waifu2x</h1> <div class="all-page">
<div class="header"> <h1 class="main-title">waifu2x</h1>
<div style="position:absolute; display:block; top:0; left:540px; max-height:140px;"> <div class="choose-lang">
<img style="position:absolute; display:block; left:0; top:0; width:149px; height:149px; border:0;" src="https://camo.githubusercontent.com/a6677b08c955af8400f44c6298f40e7d19cc5b2d/68747470733a2f2f73332e616d617a6f6e6177732e636f6d2f6769746875622f726962626f6e732f666f726b6d655f72696768745f677261795f3664366436642e706e67" alt="Fork me on GitHub" data-canonical-src="https://s3.amazonaws.com/github/ribbons/forkme_right_gray_6d6d6d.png"> <a href="index.html">
<a href="https://github.com/nagadomi/waifu2x" target="_blank" style="position:absolute; display:block; left:0; top:0; width:149px; height:130px;"></a> English
</a>
/
<a href="index.ja.html">
日本語
</a>
/
<a href="index.ru.html">
Русский
</a>
/
<a href="index.pt.html">
Português
</a>
</div> </div>
<a href="index.html">en</a>/<a href="index.ja.html">ja</a>/<a href="index.ru.html">ru</a> <p>Single-Image Super-Resolution for Anime-Style Art using Deep Convolutional Neural Networks. And it supports photo.</p>
<p class="margin1 link-box">
<a href="https://raw.githubusercontent.com/nagadomi/waifu2x/master/images/slide.png" class="blue-link" target="_blank">
Show full demonstration
</a>
|
<a href="https://github.com/nagadomi/waifu2x" class="blue-link" target="_blank">
Go to GitHub
</a>
</p>
<form action="/api" method="POST" enctype="multipart/form-data" target="_blank">
<div class="option-box first">
<div class="option-left">Image choosing:</div>
<div class="option-right">
<input type="text" id="url" name="url" placeholder="Type URL">
<div class="option-right-small">
Or choose a file:
<input type="file" id="file" name="file"></div>
</div>
<div class="option-hint">
Limits: Size: 3MB, Noise Reduction: 2560x2560px, Upscaling: 1280x1280px.
</div>
</div>
<div class="option-box">
<div class="option-left">
Style:
</div>
<div class="option-right">
<label><input type="radio" name="style" class="radio" value="art" checked>
<span class="r-text">
Artwork
</span>
</label>
<label><input type="radio" name="style" class="radio" value="photo">
<span class="r-text">
Photo
</span>
</label>
</div>
</div>
<div class="option-box">
<div class="option-left">
Noise Reduction:
<div class="option-left-small">
(expect JPEG artifact)
</div>
</div>
<div class="option-right">
<label><input type="radio" name="noise" class="radio" value="0">
<span class="r-text">
None
</span>
</label>
<label><input type="radio" name="noise" class="radio" value="1" checked>
<span class="r-text">
Medium
</span>
</label>
<label>
<input type="radio" name="noise" class="radio" value="2">
<span class="r-text">
High
</span>
</label>
</div>
<div class="option-hint">
You need use noise reduction if image actually has noise or it may cause opposite effect.
</div>
</div>
<div class="option-box">
<div class="option-left">
Upscaling:
<div class="option-left-small"></div>
</div>
<div class="option-right">
<label><input type="radio" name="scale" class="radio" value="0" checked>
<span class="r-text">
None
</span>
</label>
<label><input type="radio" name="scale" class="radio" value="1">
<span class="r-text">
1.6x
</span>
</label>
<label><input type="radio" name="scale" class="radio" value="2">
<span class="r-text">
2x
</span>
</label>
</div>
</div>
<input type="submit" class="button" value="Convert">
<input type="submit" name="download" value="Download" class="button">
<div class="bottom-hint">
<ul>
<li>If you are using Firefox, Please press the CTRL+S key to save image. "Save Image" option doesn't work.</li>
</ul>
</div>
</form>
</div> </div>
<div class="about"> <div class="bottom-info">
<div>Single-Image Super-Resolution for anime/fan-art using Deep Convolutional Neural Networks. <a href="https://raw.githubusercontent.com/nagadomi/waifu2x/master/images/slide.png" target="_blank">about</a>.</div> <a href="https://github.com/nagadomi/waifu2x" class="gray-link" target="_blank">waifu2x</a>
</div>
<form action="/api" method="POST" enctype="multipart/form-data" target="_blank">
<fieldset>
<legend>Image</legend>
<div>
URL: <input id="url" type="text" name="url" style="width:400px"> or
</div>
<div>
FILE: <input id="file" type="file" name="file">
</div>
<div class="help">
Limits: Size: 2MB, Noise Reduction: 2560x2560px, Upscaling: 1280x1280px
</div>
</fieldset>
<fieldset>
<legend>Noise Reduction (expect JPEG Artifact)</legend>
<label><input type="radio" name="noise" value="0"> None</label>
<label><input type="radio" name="noise" value="1" checked="checked"> Medium</label>
<label><input type="radio" name="noise" value="2"> High</label>
<div class="help">When using 2x scaling, we never recommend to use high level of noise reduction, it almost always makes image worse, it makes sense for only some rare cases when image had really bad quality from the beginning.</div>
</fieldset>
<fieldset>
<legend>Upscaling</legend>
<label><input type="radio" name="scale" value="0" checked="checked"> None</label>
<label><input type="radio" name="scale" value="1"> 1.6x</label>
<label><input type="radio" name="scale" value="2"> 2x</label>
</fieldset>
<input type="submit"/>
</form>
<div class="help">
<ul style="padding-left: 15px;">
<li>If you are using Firefox, Please press the CTRL+S key to save image. "Save Image" option doesn't work.
</ul>
</div> </div>
</body> </body>
</html> </html>

View file

@ -1,90 +1,146 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="ja"> <html lang="ja">
<!-- This file was automatically generated by webgen/gen.rb. Do not make changes to this file manually. -->
<head> <head>
<meta charset="UTF-8"> <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<link rel="canonical" href="http://waifu2x.udp.jp/"> <meta charset="utf-8">
<link rel="shortcut icon" href="favicon.ico"/>
<meta name="viewport" content="initial-scale=1.0,width=device-width">
<link href="//cdnjs.cloudflare.com/ajax/libs/normalize/3.0.3/normalize.min.css" rel="stylesheet" type="text/css">
<link href="style.css" rel="stylesheet" type="text/css">
<link href="mobile.css" rel="stylesheet" type="text/css" media="screen and (max-width: 768px) and (min-width: 0px)">
<script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/jquery-cookie/1.4.1/jquery.cookie.js"></script>
<script type="text/javascript" src="ui.js"></script>
<title>waifu2x</title> <title>waifu2x</title>
<style type="text/css">
body {
margin: 1em 2em 1em 2em;
background: LightGray;
width: 640px;
}
fieldset {
margin-top: 1em;
margin-bottom: 1em;
}
.about {
position: relative;
display: inline-block;
font-size: 0.8em;
padding: 1em 5px 0.2em 0;
}
.help {
font-size: 0.8em;
margin: 1em 0 0 0;
}
</style>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
<script type="text/javascript">
function clear_file() {
var new_file = $("#file").clone();
new_file.change(clear_url);
$("#file").replaceWith(new_file);
}
function clear_url() {
$("#url").val("")
}
$(function (){
$("#url").change(clear_file);
$("#file").change(clear_url);
})
</script>
</head> </head>
<body> <body>
<h1>waifu2x</h1> <div class="all-page">
<div class="header"> <h1 class="main-title">waifu2x</h1>
<div style="position:absolute; display:block; top:0; left:540px; max-height:140px;"> <div class="choose-lang">
<img style="position:absolute; display:block; left:0; top:0; width:149px; height:149px; border:0;" src="https://camo.githubusercontent.com/a6677b08c955af8400f44c6298f40e7d19cc5b2d/68747470733a2f2f73332e616d617a6f6e6177732e636f6d2f6769746875622f726962626f6e732f666f726b6d655f72696768745f677261795f3664366436642e706e67" alt="Fork me on GitHub" data-canonical-src="https://s3.amazonaws.com/github/ribbons/forkme_right_gray_6d6d6d.png"> <a href="index.html">
<a href="https://github.com/nagadomi/waifu2x" target="_blank" style="position:absolute; display:block; left:0; top:0; width:149px; height:130px;"></a> English
</a>
/
<a href="index.ja.html">
日本語
</a>
/
<a href="index.ru.html">
Русский
</a>
/
<a href="index.pt.html">
Português
</a>
</div> </div>
<a href="index.html">en</a>/<a href="index.ja.html">ja</a>/<a href="index.ru.html">ru</a> <p>深層畳み込みニューラルネットワークによる二次元画像のための超解像システム。 写真にも対応。</p>
<p class="margin1 link-box">
<a href="https://raw.githubusercontent.com/nagadomi/waifu2x/master/images/slide.png" class="blue-link" target="_blank">
実行例を表示
</a>
|
<a href="https://github.com/nagadomi/waifu2x" class="blue-link" target="_blank">
プロジェクトページ(GitHub)
</a>
</p>
<form action="/api" method="POST" enctype="multipart/form-data" target="_blank">
<div class="option-box first">
<div class="option-left">画像を選択:</div>
<div class="option-right">
<input type="text" id="url" name="url" placeholder="URLを入力">
<div class="option-right-small">
ファイルを選択:
<input type="file" id="file" name="file"></div>
</div>
<div class="option-hint">
制限: サイズ: 3MB, ノイズ除去: 2560x2560px, 拡大(前): 1280x1280px.
</div>
</div>
<div class="option-box">
<div class="option-left">
スタイル:
</div>
<div class="option-right">
<label><input type="radio" name="style" class="radio" value="art" checked>
<span class="r-text">
イラスト
</span>
</label>
<label><input type="radio" name="style" class="radio" value="photo">
<span class="r-text">
写真
</span>
</label>
</div>
</div>
<div class="option-box">
<div class="option-left">
ノイズ除去:
<div class="option-left-small">
(JPEGイズを想定)
</div>
</div>
<div class="option-right">
<label><input type="radio" name="noise" class="radio" value="0">
<span class="r-text">
なし
</span>
</label>
<label><input type="radio" name="noise" class="radio" value="1" checked>
<span class="r-text">
</span>
</label>
<label>
<input type="radio" name="noise" class="radio" value="2">
<span class="r-text">
</span>
</label>
</div>
<div class="option-hint">
イズ除去は細部が消えることがあります。JPEGイズがある場合に使用します。
</div>
</div>
<div class="option-box">
<div class="option-left">
拡大:
<div class="option-left-small"></div>
</div>
<div class="option-right">
<label><input type="radio" name="scale" class="radio" value="0" checked>
<span class="r-text">
なし
</span>
</label>
<label><input type="radio" name="scale" class="radio" value="1">
<span class="r-text">
1.6x
</span>
</label>
<label><input type="radio" name="scale" class="radio" value="2">
<span class="r-text">
2x
</span>
</label>
</div>
</div>
<input type="submit" class="button" value="実行">
<input type="submit" name="download" value="実行結果を保存" class="button">
<div class="bottom-hint">
<ul>
<li>Firefoxの方は、右クリから画像が保存できないようなので、CTRL+SキーかALTキー後 ファイル - ページを保存 で画像を保存してください。</li>
</ul>
</div>
</form>
</div> </div>
<div class="about"> <div class="bottom-info">
<div>深層畳み込みニューラルネットワークによる二次元画像のための超解像システム. <a href="https://raw.githubusercontent.com/nagadomi/waifu2x/master/images/slide.png" target="_blank">about</a>.</div> <a href="https://github.com/nagadomi/waifu2x" class="gray-link" target="_blank">waifu2x</a>
</div>
<form action="/api" method="POST" enctype="multipart/form-data" target="_blank">
<fieldset>
<legend>画像</legend>
<div>
URL: <input id="url" type="text" name="url" style="width:400px"> or
</div>
<div>
FILE: <input id="file" type="file" name="file">
</div>
<div class="help">
制限: サイズ: 2MB, ノイズ除去: 2560x2560px, 拡大: 1280x1280px
</div>
</fieldset>
<fieldset>
<legend>ノイズ除去 (JPEGイズを想定)</legend>
<label><input type="radio" name="noise" value="0"> なし</label>
<label><input type="radio" name="noise" value="1" checked="checked"></label>
<label><input type="radio" name="noise" value="2"></label>
</fieldset>
<fieldset>
<legend>拡大</legend>
<label><input type="radio" name="scale" value="0" checked="checked"> なし</label>
<label><input type="radio" name="scale" value="1"> 1.6x</label>
<label><input type="radio" name="scale" value="2"> 2x</label>
</fieldset>
<input type="submit" value="実行"/>
</form>
<div class="help">
<ul style="padding-left: 15px;">
<li>なし/なしで入力画像を変換せずに出力する。ブラウザのタブで変換結果を比較したい人用。
<li>Firefoxの方は、右クリから画像が保存できないようなので、CTRL+SキーかALTキー後 ファイル - ページを保存 で画像を保存してください。
</ul>
</div> </div>
</body> </body>
</html> </html>

146
assets/index.pt.html Normal file
View file

@ -0,0 +1,146 @@
<!DOCTYPE html>
<html lang="pt">
<!-- This file was automatically generated by webgen/gen.rb. Do not make changes to this file manually. -->
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta charset="utf-8">
<link rel="shortcut icon" href="favicon.ico"/>
<meta name="viewport" content="initial-scale=1.0,width=device-width">
<link href="//cdnjs.cloudflare.com/ajax/libs/normalize/3.0.3/normalize.min.css" rel="stylesheet" type="text/css">
<link href="style.css" rel="stylesheet" type="text/css">
<link href="mobile.css" rel="stylesheet" type="text/css" media="screen and (max-width: 768px) and (min-width: 0px)">
<script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/jquery-cookie/1.4.1/jquery.cookie.js"></script>
<script type="text/javascript" src="ui.js"></script>
<title>waifu2x</title>
</head>
<body>
<div class="all-page">
<h1 class="main-title">waifu2x</h1>
<div class="choose-lang">
<a href="index.html">
English
</a>
/
<a href="index.ja.html">
日本語
</a>
/
<a href="index.ru.html">
Русский
</a>
/
<a href="index.pt.html">
Português
</a>
</div>
<p>Single-Image Super-Resolution for Anime-Style Art using Deep Convolutional Neural Networks. And it supports photo.</p>
<p class="margin1 link-box">
<a href="https://raw.githubusercontent.com/nagadomi/waifu2x/master/images/slide.png" class="blue-link" target="_blank">
Sobre
</a>
|
<a href="https://github.com/nagadomi/waifu2x" class="blue-link" target="_blank">
Ir para Github
</a>
</p>
<form action="/api" method="POST" enctype="multipart/form-data" target="_blank">
<div class="option-box first">
<div class="option-left">Imagem:</div>
<div class="option-right">
<input type="text" id="url" name="url" placeholder="URL">
<div class="option-right-small">
ARQUIVO:
<input type="file" id="file" name="file"></div>
</div>
<div class="option-hint">
Limites: Tamanho: 3MB, Redução de ruído: 2560x2560px, Aumento de escala: 1280x1280px.
</div>
</div>
<div class="option-box">
<div class="option-left">
Estilo:
</div>
<div class="option-right">
<label><input type="radio" name="style" class="radio" value="art" checked>
<span class="r-text">
Arte
</span>
</label>
<label><input type="radio" name="style" class="radio" value="photo">
<span class="r-text">
Foto
</span>
</label>
</div>
</div>
<div class="option-box">
<div class="option-left">
Redução de ruído:
<div class="option-left-small">
(Exceto artefato JPEG)
</div>
</div>
<div class="option-right">
<label><input type="radio" name="noise" class="radio" value="0">
<span class="r-text">
Nenhuma
</span>
</label>
<label><input type="radio" name="noise" class="radio" value="1" checked>
<span class="r-text">
Média
</span>
</label>
<label>
<input type="radio" name="noise" class="radio" value="2">
<span class="r-text">
Alta
</span>
</label>
</div>
<div class="option-hint">
Quando usando a escala 2x, Nós nunca recomendamos usar um nível alto de redução de ruído, quase sempre deixa a imagem pior, faz sentido apenas para casos raros quando a imagem tinha uma qualidade muito má desde o começo.
</div>
</div>
<div class="option-box">
<div class="option-left">
Aumento de escala:
<div class="option-left-small"></div>
</div>
<div class="option-right">
<label><input type="radio" name="scale" class="radio" value="0" checked>
<span class="r-text">
Nenhum
</span>
</label>
<label><input type="radio" name="scale" class="radio" value="1">
<span class="r-text">
1.6x
</span>
</label>
<label><input type="radio" name="scale" class="radio" value="2">
<span class="r-text">
2x
</span>
</label>
</div>
</div>
<input type="submit" class="button" value="Converter">
<input type="submit" name="download" value="Baixar" class="button">
<div class="bottom-hint">
<ul>
<li>Se Você estiver usando o Firefox, por favor, aperte CTRL+S para salvar a imagem. A opção "Salvar Imagem" não funciona</li>
</ul>
</div>
</form>
</div>
<div class="bottom-info">
<a href="https://github.com/nagadomi/waifu2x" class="gray-link" target="_blank">waifu2x</a>
</div>
</body>
</html>

View file

@ -1,90 +1,146 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html lang="ru">
<!-- This file was automatically generated by webgen/gen.rb. Do not make changes to this file manually. -->
<head> <head>
<meta charset="UTF-8"> <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<link rel="canonical" href="http://waifu2x.udp.jp/"> <meta charset="utf-8">
<link rel="shortcut icon" href="favicon.ico"/>
<meta name="viewport" content="initial-scale=1.0,width=device-width">
<link href="//cdnjs.cloudflare.com/ajax/libs/normalize/3.0.3/normalize.min.css" rel="stylesheet" type="text/css">
<link href="style.css" rel="stylesheet" type="text/css">
<link href="mobile.css" rel="stylesheet" type="text/css" media="screen and (max-width: 768px) and (min-width: 0px)">
<script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/jquery-cookie/1.4.1/jquery.cookie.js"></script>
<script type="text/javascript" src="ui.js"></script>
<title>waifu2x</title> <title>waifu2x</title>
<style type="text/css">
body {
margin: 1em 2em 1em 2em;
background: LightGray;
width: 640px;
}
fieldset {
margin-top: 1em;
margin-bottom: 1em;
}
.about {
position: relative;
display: inline-block;
font-size: 0.9em;
padding: 1em 5px 0.2em 0;
}
.help {
font-size: 0.85em;
margin: 1em 0 0 0;
}
</style>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
<script type="text/javascript">
function clear_file() {
var new_file = $("#file").clone();
new_file.change(clear_url);
$("#file").replaceWith(new_file);
}
function clear_url() {
$("#url").val("")
}
$(function (){
$("#url").change(clear_file);
$("#file").change(clear_url);
})
</script>
</head> </head>
<body> <body>
<h1>waifu2x</h1> <div class="all-page">
<div class="header"> <h1 class="main-title">waifu2x</h1>
<div style="position:absolute; display:block; top:0; left:540px; max-height:140px;"> <div class="choose-lang">
<img style="position:absolute; display:block; left:0; top:0; width:149px; height:149px; border:0;" src="https://camo.githubusercontent.com/a6677b08c955af8400f44c6298f40e7d19cc5b2d/68747470733a2f2f73332e616d617a6f6e6177732e636f6d2f6769746875622f726962626f6e732f666f726b6d655f72696768745f677261795f3664366436642e706e67" alt="Fork me on GitHub" data-canonical-src="https://s3.amazonaws.com/github/ribbons/forkme_right_gray_6d6d6d.png"> <a href="index.html">
<a href="https://github.com/nagadomi/waifu2x" target="_blank" style="position:absolute; display:block; left:0; top:0; width:149px; height:130px;"></a> English
</a>
/
<a href="index.ja.html">
日本語
</a>
/
<a href="index.ru.html">
Русский
</a>
/
<a href="index.pt.html">
Português
</a>
</div> </div>
<a href="index.html">en</a>/<a href="index.ja.html">ja</a>/<a href="index.ru.html">ru</a> <p>Waifu2x позволяет увеличивать в 4 раза рисованные изображения, например аниме или арт, а также устранять шум на изображении (преимущественно артефакты сжатия JPEG). Теперь также поддерживаются фотографии.</p>
<p class="margin1 link-box">
<a href="https://raw.githubusercontent.com/nagadomi/waifu2x/master/images/slide.png" class="blue-link" target="_blank">
Посмотреть полную демонстрацию
</a>
|
<a href="https://github.com/nagadomi/waifu2x" class="blue-link" target="_blank">
Перейти на GitHub
</a>
</p>
<form action="/api" method="POST" enctype="multipart/form-data" target="_blank">
<div class="option-box first">
<div class="option-left">Выбор изображения:</div>
<div class="option-right">
<input type="text" id="url" name="url" placeholder="Укажите URL">
<div class="option-right-small">
Либо выберите файл:
<input type="file" id="file" name="file"></div>
</div>
<div class="option-hint">
Макс. размер файла — 3MB, устранение шума — макс. 2560x2560px, апскейл — 1280x1280px.
</div>
</div>
<div class="option-box">
<div class="option-left">
Тип изображения:
</div>
<div class="option-right">
<label><input type="radio" name="style" class="radio" value="art" checked>
<span class="r-text">
Арт
</span>
</label>
<label><input type="radio" name="style" class="radio" value="photo">
<span class="r-text">
Фотография
</span>
</label>
</div>
</div>
<div class="option-box">
<div class="option-left">
Устранение шума:
<div class="option-left-small">
(артефактов JPEG)
</div>
</div>
<div class="option-right">
<label><input type="radio" name="noise" class="radio" value="0">
<span class="r-text">
Нет
</span>
</label>
<label><input type="radio" name="noise" class="radio" value="1" checked>
<span class="r-text">
Средне
</span>
</label>
<label>
<input type="radio" name="noise" class="radio" value="2">
<span class="r-text">
Сильно
</span>
</label>
</div>
<div class="option-hint">
Устранение шума нужно использовать, если на картинке действительно есть шум, иначе это даст противоположный эффект.
</div>
</div>
<div class="option-box">
<div class="option-left">
Апскейл:
<div class="option-left-small"></div>
</div>
<div class="option-right">
<label><input type="radio" name="scale" class="radio" value="0" checked>
<span class="r-text">
Нет
</span>
</label>
<label><input type="radio" name="scale" class="radio" value="1">
<span class="r-text">
1.6x
</span>
</label>
<label><input type="radio" name="scale" class="radio" value="2">
<span class="r-text">
2x
</span>
</label>
</div>
</div>
<input type="submit" class="button" value="Преобразовать">
<input type="submit" name="download" value="Скачать" class="button">
<div class="bottom-hint">
<ul>
<li>Если Вы используете Firefox, для сохранения изображения нажмите Ctrl+S (перетаскивание изображения и опция "Сохранить изображение" работать не будут).</li>
</ul>
</div>
</form>
</div> </div>
<div class="about"> <div class="bottom-info">
<div>Увеличение в 4 раза рисованных изображений, например, аниме или фан-арт, а также устранение шума (преимущественно артефактов сжатия JPEG), см. <a href="https://raw.githubusercontent.com/nagadomi/waifu2x/master/images/slide.png" target="_blank">демонстрацию и сравнения</a></div> <a href="https://github.com/nagadomi/waifu2x" class="gray-link" target="_blank">waifu2x</a>
</div>
<form action="/api" method="POST" enctype="multipart/form-data" target="_blank">
<fieldset>
<legend>Выбор изображения</legend>
<div>
Указать URL: <input id="url" type="text" name="url" style="width:400px">
</div>
<div>
Или загрузить файл: <input id="file" type="file" name="file">
</div>
<div class="help">
Макс. размер файла — 2MB, устранение шума — макс. 2560x2560px, апскейл — 1280x1280px
</div>
</fieldset>
<fieldset>
<legend>Устранение шума (артефактов JPEG)</legend>
<label><input type="radio" name="noise" value="0"> Нет</label>
<label><input type="radio" name="noise" value="1" checked="checked"> Средне</label>
<label><input type="radio" name="noise" value="2"> Сильно (не рекомендуется)</label>
<div class="help">Устранение шума нужно использовать, если на картинке действительно есть шум, иначе это даст противоположный эффект. Также не рекомендуется сильное устранение шума, оно даёт выгоду только в редких случаях, когда картинка изначально была сильно испорчена.</div>
</fieldset>
<fieldset>
<legend>Апскейл (увеличение размера)</legend>
<label><input type="radio" name="scale" value="0" checked="checked"> Нет</label>
<label><input type="radio" name="scale" value="1"> 1.6x</label>
<label><input type="radio" name="scale" value="2"> 2x</label>
</fieldset>
<input type="submit"/>
</form>
<div class="help">
<ul style="padding-left: 15px;">
<li>Если Вы используете Firefox, для сохранения изображения Вам придётся нажать Ctrl+S (опция в меню "Сохранить изображение" работать не будет!)
</ul>
</div> </div>
</body> </body>
</html> </html>

28
assets/mobile.css Normal file
View file

@ -0,0 +1,28 @@
body {
width: 98%;
font-size: 100%;
}
.all-page {
width: auto;
margin: 1em auto;
padding: 1em;
}
.main-title {
display: block;
}
.option-left {
width: auto;
display: block;
}
#url {
width: 100%;
height: 2em;
}
.option-right {
display: block;
}
.button {
min-width: 10px;
width: 100%;
height: 3em;
}

191
assets/style.css Normal file
View file

@ -0,0 +1,191 @@
button::-moz-focus-inner,
input[type="reset"]::-moz-focus-inner,
input[type="button"]::-moz-focus-inner,
input[type="submit"]::-moz-focus-inner,
input[type="submit"]::-moz-focus-inner,
input[type="file"] > input[type="button"]::-moz-focus-inner
{
border: none;
}
input[type="checkbox"]:focus {
-moz-outline-offset: -1px !important;
-moz-outline: 1px solid #000 !important;
}
:focus {
outline: none;
} /*Remove a dotted line around 1) buttons, 2) checkboxes, 3) links*/
a {
text-decoration: none;
cursor: pointer;
color: inherit;
}
a:hover {
text-decoration: underline;
}
div, span, a, input {
background-repeat: no-repeat;
}
body {
width: 782px;
margin: 0 auto;
background: #ccc url(bg.png) no-repeat center bottom;
color: #000;
font-size: 14px;
font-family: Tahoma, Arial, Verdana, Meiryo, "MS Gothic", sans-serif, Lucida Sans;
line-height: 1.5em;
text-align: center;
}
.all-page {
position: relative;
width: 690px;
margin: 15px auto;
padding: 10px 30px 15px 30px;
background: #eee;
border: 2px solid #999;
border-radius: 8px;
text-align: left;
}
.all-page:after {
content: "";
position: absolute;
left: -1px;
top: -1px;
width: 100%;
height: 100%;
height: calc(100% - 2px);
padding: 0 1px;
box-shadow: 0px 5px 8px #bbb;
z-index: -1;
} /*for crop shadow bottom for 4px (2px from border and 2px from calc)*/
.main-title {
font-size: 2em;
font-weight: bold;
margin: 0.6em 0;
white-space: nowrap;
display: inline-block;
}
.choose-lang {
font-size: 0.8em;
margin: 0 5px;
opacity: 0.9;
vertical-align: middle;
}
p {
margin: 0.4em 0;
}
p.margin1 { margin: 0.9em 0; }
.links-box {
color: #999;
}
.example {
width: 445px;
height: 200px;
}
.blue-link {
color: #36b;
}
.gray-link {
color: #999;
}
.second-title {
font-size: 1.5em;
font-weight: bold;
margin: 1em 0 1em;
line-height: 1.3em;
}
.option-box {
margin: 1.5em 0;
white-space: nowrap;
}
.option-left {
display: inline-block;
width: 180px;
color: #707070;
font-weight: bold;
}
.option-left-small {
font-size: 0.8em;
line-height: 1.5em;
}
.option-right {
display: inline-block;
white-space: normal;
vertical-align: top;
}
.option-right-small {
margin-top: 2px;
font-size: 0.9em;
}
.option-hint {
margin: 0.5em 0;
color: #888;
font-size: 0.85em;
line-height: 1.5em;
white-space: normal;
}
#url {
width: 300px;
height: 23px;
padding: 0 3px;
border: 1px solid #b0b0b0;
}
label {
margin: 0 5px 0 0;
padding: 0;
cursor: pointer;
}
.radio {
margin: 0 4px 0 0;
padding: 0;
cursor: pointer;
vertical-align: middle;
}
.r-text {
vertical-align: middle;
}
.radio:checked + .r-text { color: #494; }
.button {
min-width: 160px;
height: 26px;
margin: 0 10px 3px 0;
padding-bottom: 1px;
background: #f2f2f2;
background-image: linear-gradient(to bottom, #f9f9f9, #dadada);
border: 1px solid #999;
border-radius: 1px;
cursor: pointer;
}
.button:hover {
background: #f7f7f7;
background-image: linear-gradient(to bottom, #fefefe, #e2e2e2);
}
.bottom-hint {
margin: 0.85em 0;
color: #888;
font-size: 0.85em;
line-height: 1.5em;
text-align: center;
}

52
assets/ui.js Normal file
View file

@ -0,0 +1,52 @@
$(function (){
var expires = 365;
function clear_file() {
var new_file = $("#file").clone();
new_file.change(clear_url);
$("#file").replaceWith(new_file);
}
function clear_url() {
$("#url").val("")
}
function on_change_style(e) {
var checked = $("input[name=style]:checked");
if (checked.val() == "art") {
$(".main-title").text("waifu2x");
} else {
$(".main-title").html("w<s>/a/</s>ifu2x");
}
$.cookie("style", checked.val(), {expires: expires});
}
function on_change_noise_level(e)
{
var checked = $("input[name=noise]:checked");
$.cookie("noise", checked.val(), {expires: expires});
}
function on_change_scale_factor(e)
{
var checked = $("input[name=scale]:checked");
$.cookie("scale", checked.val(), {expires: expires});
}
function restore_from_cookie()
{
if ($.cookie("style")) {
$("input[name=style]").filter("[value=" + $.cookie("style") + "]").prop("checked", true)
}
if ($.cookie("noise")) {
$("input[name=noise]").filter("[value=" + $.cookie("noise") + "]").prop("checked", true)
}
if ($.cookie("scale")) {
$("input[name=scale]").filter("[value=" + $.cookie("scale") + "]").prop("checked", true)
}
}
$("#url").change(clear_file);
$("#file").change(clear_url);
$("input[name=style]").change(on_change_style);
$("input[name=noise]").change(on_change_noise_level);
$("input[name=scale]").change(on_change_scale_factor);
restore_from_cookie();
on_change_style();
on_change_scale_factor();
on_change_noise_level();
})

View file

@ -1,48 +1,47 @@
require './lib/portable' local __FILE__ = (function() return string.gsub(debug.getinfo(2, 'S').source, "^@", "") end)()
package.path = path.join(path.dirname(__FILE__), "lib", "?.lua;") .. package.path
require 'pl'
require 'image' require 'image'
local settings = require './lib/settings' local compression = require 'compression'
local image_loader = require './lib/image_loader' local settings = require 'settings'
local image_loader = require 'image_loader'
local function count_lines(file) local iproc = require 'iproc'
local fp = io.open(file, "r")
local count = 0
for line in fp:lines() do
count = count + 1
end
fp:close()
return count
end
local function crop_4x(x)
local w = x:size(3) % 4
local h = x:size(2) % 4
return image.crop(x, 0, 0, x:size(3) - w, x:size(2) - h)
end
local function load_images(list) local function load_images(list)
local count = count_lines(list) local MARGIN = 32
local fp = io.open(list, "r") local lines = utils.split(file.read(list), "\n")
local x = {} local x = {}
local c = 0 for i = 1, #lines do
for line in fp:lines() do local line = lines[i]
local im = crop_4x(image_loader.load_byte(line)) local im, alpha = image_loader.load_byte(line)
if im then if alpha then
if im:size(2) >= settings.crop_size * 2 and im:size(3) >= settings.crop_size * 2 then io.stderr:write(string.format("\n%s: skip: image has alpha channel.\n", line))
table.insert(x, im)
end
else else
print("error:" .. line) im = iproc.crop_mod4(im)
local scale = 1.0
if settings.random_half_rate > 0.0 then
scale = 2.0
end
if im then
if im:size(2) > (settings.crop_size * scale + MARGIN) and im:size(3) > (settings.crop_size * scale + MARGIN) then
table.insert(x, compression.compress(im))
else
io.stderr:write(string.format("\n%s: skip: image is too small (%d > size).\n", line, settings.crop_size * scale + MARGIN))
end
else
io.stderr:write(string.format("\n%s: skip: load error.\n", line))
end
end end
c = c + 1 xlua.progress(i, #lines)
xlua.progress(c, count) if i % 10 == 0 then
if c % 10 == 0 then
collectgarbage() collectgarbage()
end end
end end
return x return x
end end
torch.manualSeed(settings.seed)
print(settings) print(settings)
local x = load_images(settings.image_list) local x = load_images(settings.image_list)
torch.save(settings.images, x) torch.save(settings.images, x)

View file

@ -1,34 +0,0 @@
require 'cunn'
require 'cudnn'
require 'cutorch'
require './lib/LeakyReLU'
local srcnn = require 'lib/srcnn'
local function cudnn2cunn(cudnn_model)
local cunn_model = srcnn.waifu2x("y")
local from_seq = cudnn_model:findModules("cudnn.SpatialConvolution")
local to_seq = cunn_model:findModules("nn.SpatialConvolutionMM")
for i = 1, #from_seq do
local from = from_seq[i]
local to = to_seq[i]
to.weight:copy(from.weight)
to.bias:copy(from.bias)
end
cunn_model:cuda()
cunn_model:evaluate()
return cunn_model
end
local cmd = torch.CmdLine()
cmd:text()
cmd:text("convert cudnn model to cunn model ")
cmd:text("Options:")
cmd:option("-model", "./model.t7", 'path of cudnn model file')
cmd:option("-iformat", "ascii", 'input format')
cmd:option("-oformat", "ascii", 'output format')
local opt = cmd:parse(arg)
local cudnn_model = torch.load(opt.model, opt.iformat)
local cunn_model = cudnn2cunn(cudnn_model)
torch.save(opt.model, cunn_model, opt.oformat)

View file

@ -1,23 +0,0 @@
-- adapted from https://github.com/marcan/cl-waifu2x
require './lib/portable'
require './lib/LeakyReLU'
local cjson = require "cjson"
local model = torch.load(arg[1], "ascii")
local jmodules = {}
local modules = model:findModules("nn.SpatialConvolutionMM")
for i = 1, #modules, 1 do
local module = modules[i]
local jmod = {
kW = module.kW,
kH = module.kH,
nInputPlane = module.nInputPlane,
nOutputPlane = module.nOutputPlane,
bias = torch.totable(module.bias:float()),
weight = torch.totable(module.weight:float():reshape(module.nOutputPlane, module.nInputPlane, module.kW, module.kH))
}
table.insert(jmodules, jmod)
end
io.write(cjson.encode(jmodules))

View file

@ -1,8 +1,7 @@
#!/bin/sh #!/bin/sh
th waifu2x.lua -noise_level 1 -m noise_scale -i images/miku_small.png -o images/miku_small_waifu2x.png th waifu2x.lua -m scale -i images/miku_small.png -o images/miku_small_waifu2x.png
th waifu2x.lua -noise_level 2 -m noise_scale -i images/miku_small_noisy.png -o images/miku_small_noisy_waifu2x.png th waifu2x.lua -noise_level 2 -m noise_scale -i images/miku_small_noisy.png -o images/miku_small_noisy_waifu2x.png
th waifu2x.lua -noise_level 2 -m noise -i images/miku_noisy.png -o images/miku_noisy_waifu2x.png th waifu2x.lua -noise_level 2 -m noise -i images/miku_noisy.png -o images/miku_noisy_waifu2x.png
th waifu2x.lua -noise_level 2 -m noise_scale -i images/miku_CC_BY-NC_noisy.jpg -o images/miku_CC_BY-NC_noisy_waifu2x.png
th waifu2x.lua -noise_level 2 -m noise -i images/lena.png -o images/lena_waifu2x.png th waifu2x.lua -noise_level 2 -m noise -i images/lena.png -o images/lena_waifu2x.png
th waifu2x.lua -m scale -model_dir models/ukbench -i images/lena.png -o images/lena_waifu2x_ukbench.png th waifu2x.lua -m scale -model_dir models/ukbench -i images/lena.png -o images/lena_waifu2x_ukbench.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 315 KiB

After

Width:  |  Height:  |  Size: 397 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 605 KiB

After

Width:  |  Height:  |  Size: 651 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 177 KiB

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 499 KiB

After

Width:  |  Height:  |  Size: 493 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 380 KiB

After

Width:  |  Height:  |  Size: 368 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 378 KiB

After

Width:  |  Height:  |  Size: 352 KiB

View file

@ -0,0 +1,39 @@
-- ref: https://en.wikipedia.org/wiki/Huber_loss
local ClippedWeightedHuberCriterion, parent = torch.class('w2nn.ClippedWeightedHuberCriterion','nn.Criterion')
function ClippedWeightedHuberCriterion:__init(w, gamma, clip)
parent.__init(self)
self.clip = clip
self.gamma = gamma or 1.0
self.weight = w:clone()
self.diff = torch.Tensor()
self.diff_abs = torch.Tensor()
--self.outlier_rate = 0.0
self.square_loss_buff = torch.Tensor()
self.linear_loss_buff = torch.Tensor()
end
function ClippedWeightedHuberCriterion:updateOutput(input, target)
self.diff:resizeAs(input):copy(input)
self.diff[torch.lt(self.diff, self.clip[1])] = self.clip[1]
self.diff[torch.gt(self.diff, self.clip[2])] = self.clip[2]
for i = 1, input:size(1) do
self.diff[i]:add(-1, target[i]):cmul(self.weight)
end
self.diff_abs:resizeAs(self.diff):copy(self.diff):abs()
local square_targets = self.diff[torch.lt(self.diff_abs, self.gamma)]
local linear_targets = self.diff[torch.ge(self.diff_abs, self.gamma)]
local square_loss = self.square_loss_buff:resizeAs(square_targets):copy(square_targets):pow(2.0):mul(0.5):sum()
local linear_loss = self.linear_loss_buff:resizeAs(linear_targets):copy(linear_targets):abs():add(-0.5 * self.gamma):mul(self.gamma):sum()
--self.outlier_rate = linear_targets:nElement() / input:nElement()
self.output = (square_loss + linear_loss) / input:nElement()
return self.output
end
function ClippedWeightedHuberCriterion:updateGradInput(input, target)
local norm = 1.0 / input:nElement()
self.gradInput:resizeAs(self.diff):copy(self.diff):mul(norm)
local outlier = torch.ge(self.diff_abs, self.gamma)
self.gradInput[outlier] = torch.sign(self.diff[outlier]) * self.gamma * norm
return self.gradInput
end

77
lib/DepthExpand2x.lua Normal file
View file

@ -0,0 +1,77 @@
if w2nn.DepthExpand2x then
return w2nn.DepthExpand2x
end
local DepthExpand2x, parent = torch.class('w2nn.DepthExpand2x','nn.Module')
function DepthExpand2x:__init()
parent:__init()
end
function DepthExpand2x:updateOutput(input)
local x = input
-- (batch_size, depth, height, width)
self.shape = x:size()
assert(self.shape:size() == 4, "input must be 4d tensor")
assert(self.shape[2] % 4 == 0, "depth must be depth % 4 = 0")
-- (batch_size, width, height, depth)
x = x:transpose(2, 4)
-- (batch_size, width, height * 2, depth / 2)
x = x:reshape(self.shape[1], self.shape[4], self.shape[3] * 2, self.shape[2] / 2)
-- (batch_size, height * 2, width, depth / 2)
x = x:transpose(2, 3)
-- (batch_size, height * 2, width * 2, depth / 4)
x = x:reshape(self.shape[1], self.shape[3] * 2, self.shape[4] * 2, self.shape[2] / 4)
-- (batch_size, depth / 4, height * 2, width * 2)
x = x:transpose(2, 4)
x = x:transpose(3, 4)
self.output:resizeAs(x):copy(x) -- contiguous
return self.output
end
function DepthExpand2x:updateGradInput(input, gradOutput)
-- (batch_size, depth / 4, height * 2, width * 2)
local x = gradOutput
-- (batch_size, height * 2, width * 2, depth / 4)
x = x:transpose(2, 4)
x = x:transpose(2, 3)
-- (batch_size, height * 2, width, depth / 2)
x = x:reshape(self.shape[1], self.shape[3] * 2, self.shape[4], self.shape[2] / 2)
-- (batch_size, width, height * 2, depth / 2)
x = x:transpose(2, 3)
-- (batch_size, width, height, depth)
x = x:reshape(self.shape[1], self.shape[4], self.shape[3], self.shape[2])
-- (batch_size, depth, height, width)
x = x:transpose(2, 4)
self.gradInput:resizeAs(x):copy(x)
return self.gradInput
end
function DepthExpand2x.test()
require 'image'
local function show(x)
local img = torch.Tensor(3, x:size(3), x:size(4))
img[1]:copy(x[1][1])
img[2]:copy(x[1][2])
img[3]:copy(x[1][3])
image.display(img)
end
local img = image.lena()
local x = torch.Tensor(1, img:size(1) * 4, img:size(2), img:size(3))
for i = 0, img:size(1) * 4 - 1 do
src_index = ((i % 3) + 1)
x[1][i + 1]:copy(img[src_index])
end
show(x)
local de2x = w2nn.DepthExpand2x()
out = de2x:forward(x)
show(out)
out = de2x:updateGradInput(x, out)
show(out)
end
return DepthExpand2x

View file

@ -1,7 +1,8 @@
if nn.LeakyReLU then if w2nn and w2nn.LeakyReLU then
return return w2nn.LeakyReLU
end end
local LeakyReLU, parent = torch.class('nn.LeakyReLU','nn.Module')
local LeakyReLU, parent = torch.class('w2nn.LeakyReLU','nn.Module')
function LeakyReLU:__init(negative_scale) function LeakyReLU:__init(negative_scale)
parent.__init(self) parent.__init(self)

View file

@ -0,0 +1,31 @@
if nn.LeakyReLU then
return nn.LeakyReLU
end
local LeakyReLU, parent = torch.class('nn.LeakyReLU','nn.Module')
function LeakyReLU:__init(negative_scale)
parent.__init(self)
self.negative_scale = negative_scale or 0.333
self.negative = torch.Tensor()
end
function LeakyReLU:updateOutput(input)
self.output:resizeAs(input):copy(input):abs():add(input):div(2)
self.negative:resizeAs(input):copy(input):abs():add(-1.0, input):mul(-0.5*self.negative_scale)
self.output:add(self.negative)
return self.output
end
function LeakyReLU:updateGradInput(input, gradOutput)
self.gradInput:resizeAs(gradOutput)
-- filter positive
self.negative:sign():add(1)
torch.cmul(self.gradInput, gradOutput, self.negative)
-- filter negative
self.negative:add(-1):mul(-1 * self.negative_scale):cmul(gradOutput)
self.gradInput:add(self.negative)
return self.gradInput
end

View file

@ -0,0 +1,25 @@
local WeightedMSECriterion, parent = torch.class('w2nn.WeightedMSECriterion','nn.Criterion')
function WeightedMSECriterion:__init(w)
parent.__init(self)
self.weight = w:clone()
self.diff = torch.Tensor()
self.loss = torch.Tensor()
end
function WeightedMSECriterion:updateOutput(input, target)
self.diff:resizeAs(input):copy(input)
for i = 1, input:size(1) do
self.diff[i]:add(-1, target[i]):cmul(self.weight)
end
self.loss:resizeAs(self.diff):copy(self.diff):cmul(self.diff)
self.output = self.loss:mean()
return self.output
end
function WeightedMSECriterion:updateGradInput(input, target)
local norm = 2.0 / input:nElement()
self.gradInput:resizeAs(input):copy(self.diff):mul(norm)
return self.gradInput
end

80
lib/alpha_util.lua Normal file
View file

@ -0,0 +1,80 @@
local w2nn = require 'w2nn'
local reconstruct = require 'reconstruct'
local image = require 'image'
local iproc = require 'iproc'
local gm = require 'graphicsmagick'
alpha_util = {}
function alpha_util.make_border(rgb, alpha, offset)
if not alpha then
return rgb
end
local sum2d = nn.SpatialConvolutionMM(1, 1, 3, 3, 1, 1, 1, 1):cuda()
sum2d.weight:fill(1)
sum2d.bias:zero()
local mask = alpha:clone()
mask[torch.gt(mask, 0.0)] = 1
mask[torch.eq(mask, 0.0)] = 0
local mask_nega = (mask - 1):abs():byte()
local eps = 1.0e-7
rgb = rgb:clone()
rgb[1][mask_nega] = 0
rgb[2][mask_nega] = 0
rgb[3][mask_nega] = 0
for i = 1, offset do
local mask_weight = sum2d:forward(mask:cuda()):float()
local border = rgb:clone()
for j = 1, 3 do
border[j]:copy(sum2d:forward(rgb[j]:reshape(1, rgb:size(2), rgb:size(3)):cuda()))
border[j]:cdiv((mask_weight + eps))
rgb[j][mask_nega] = border[j][mask_nega]
end
mask = mask_weight:clone()
mask[torch.gt(mask_weight, 0.0)] = 1
mask_nega = (mask - 1):abs():byte()
end
rgb[torch.gt(rgb, 1.0)] = 1.0
rgb[torch.lt(rgb, 0.0)] = 0.0
return rgb
end
function alpha_util.composite(rgb, alpha, model2x)
if not alpha then
return rgb
end
if not (alpha:size(2) == rgb:size(2) and alpha:size(3) == rgb:size(3)) then
if model2x then
alpha = reconstruct.scale(model2x, 2.0, alpha)
else
alpha = gm.Image(alpha, "I", "DHW"):size(rgb:size(3), rgb:size(2), "Sinc"):toTensor("float", "I", "DHW")
end
end
local out = torch.Tensor(4, rgb:size(2), rgb:size(3))
out[1]:copy(rgb[1])
out[2]:copy(rgb[2])
out[3]:copy(rgb[3])
out[4]:copy(alpha)
return out
end
local function test()
require 'sys'
require 'trepl'
torch.setdefaulttensortype("torch.FloatTensor")
local image_loader = require 'image_loader'
local rgb, alpha = image_loader.load_float("alpha.png")
local t = sys.clock()
rgb = alpha_util.make_border(rgb, alpha, 7)
print(sys.clock() - t)
print(rgb:min(), rgb:max())
image.display({image = rgb, min = 0, max = 1})
image.save("out.png", rgb)
end
--test()
return alpha_util

View file

@ -1,9 +1,5 @@
require './lib/portable'
require './lib/LeakyReLU'
torch.setdefaulttensortype("torch.FloatTensor")
-- ref: https://github.com/torch/nn/issues/112#issuecomment-64427049 -- ref: https://github.com/torch/nn/issues/112#issuecomment-64427049
local function zeroDataSize(data) local function zeroDataSize(data)
if type(data) == 'table' then if type(data) == 'table' then
for i = 1, #data do for i = 1, #data do
@ -14,7 +10,6 @@ local function zeroDataSize(data)
end end
return data return data
end end
-- Resize the output, gradInput, etc temporary tensors to zero (so that the -- Resize the output, gradInput, etc temporary tensors to zero (so that the
-- on disk size is smaller) -- on disk size is smaller)
local function cleanupModel(node) local function cleanupModel(node)
@ -27,7 +22,7 @@ local function cleanupModel(node)
if node.finput ~= nil then if node.finput ~= nil then
node.finput = zeroDataSize(node.finput) node.finput = zeroDataSize(node.finput)
end end
if tostring(node) == "nn.LeakyReLU" then if tostring(node) == "nn.LeakyReLU" or tostring(node) == "w2nn.LeakyReLU" then
if node.negative ~= nil then if node.negative ~= nil then
node.negative = zeroDataSize(node.negative) node.negative = zeroDataSize(node.negative)
end end
@ -46,23 +41,8 @@ local function cleanupModel(node)
end end
end end
end end
collectgarbage()
end end
function w2nn.cleanup_model(model)
local cmd = torch.CmdLine()
cmd:text()
cmd:text("cleanup model")
cmd:text("Options:")
cmd:option("-model", "./model.t7", 'path of model file')
cmd:option("-iformat", "binary", 'input format')
cmd:option("-oformat", "binary", 'output format')
local opt = cmd:parse(arg)
local model = torch.load(opt.model, opt.iformat)
if model then
cleanupModel(model) cleanupModel(model)
torch.save(opt.model, model, opt.oformat) return model
else
error("model not found")
end end

17
lib/compression.lua Normal file
View file

@ -0,0 +1,17 @@
-- snapply compression for ByteTensor
require 'snappy'
local compression = {}
compression.compress = function (bt)
local enc = snappy.compress(bt:storage():string())
return {bt:size(), torch.ByteStorage():string(enc)}
end
compression.decompress = function(data)
local size = data[1]
local dec = snappy.decompress(data[2]:string())
local bt = torch.ByteTensor(unpack(torch.totable(size)))
bt:storage():string(dec)
return bt
end
return compression

124
lib/data_augmentation.lua Normal file
View file

@ -0,0 +1,124 @@
require 'image'
local iproc = require 'iproc'
local gm = require 'graphicsmagick'
local data_augmentation = {}
local function pcacov(x)
local mean = torch.mean(x, 1)
local xm = x - torch.ger(torch.ones(x:size(1)), mean:squeeze())
local c = torch.mm(xm:t(), xm)
c:div(x:size(1) - 1)
local ce, cv = torch.symeig(c, 'V')
return ce, cv
end
function data_augmentation.color_noise(src, p, factor)
factor = factor or 0.1
if torch.uniform() < p then
local src, conversion = iproc.byte2float(src)
local src_t = src:reshape(src:size(1), src:nElement() / src:size(1)):t():contiguous()
local ce, cv = pcacov(src_t)
local color_scale = torch.Tensor(3):uniform(1 / (1 + factor), 1 + factor)
pca_space = torch.mm(src_t, cv):t():contiguous()
for i = 1, 3 do
pca_space[i]:mul(color_scale[i])
end
local dest = torch.mm(pca_space:t(), cv:t()):t():contiguous():resizeAs(src)
dest[torch.lt(dest, 0.0)] = 0.0
dest[torch.gt(dest, 1.0)] = 1.0
if conversion then
dest = iproc.float2byte(dest)
end
return dest
else
return src
end
end
function data_augmentation.overlay(src, p)
if torch.uniform() < p then
local r = torch.uniform()
local src, conversion = iproc.byte2float(src)
src = src:contiguous()
local flip = data_augmentation.flip(src)
flip:mul(r):add(src * (1.0 - r))
if conversion then
flip = iproc.float2byte(flip)
end
return flip
else
return src
end
end
function data_augmentation.unsharp_mask(src, p)
if torch.uniform() < p then
local radius = 0 -- auto
local sigma = torch.uniform(0.5, 1.5)
local amount = torch.uniform(0.1, 0.9)
local threshold = torch.uniform(0.0, 0.05)
local unsharp = gm.Image(src, "RGB", "DHW"):
unsharpMask(radius, sigma, amount, threshold):
toTensor("float", "RGB", "DHW")
if src:type() == "torch.ByteTensor" then
return iproc.float2byte(unsharp)
else
return unsharp
end
else
return src
end
end
function data_augmentation.shift_1px(src)
-- reducing the even/odd issue in nearest neighbor scaler.
local direction = torch.random(1, 4)
local x_shift = 0
local y_shift = 0
if direction == 1 then
x_shift = 1
y_shift = 0
elseif direction == 2 then
x_shift = 0
y_shift = 1
elseif direction == 3 then
x_shift = 1
y_shift = 1
elseif flip == 4 then
x_shift = 0
y_shift = 0
end
local w = src:size(3) - x_shift
local h = src:size(2) - y_shift
w = w - (w % 4)
h = h - (h % 4)
local dest = iproc.crop(src, x_shift, y_shift, x_shift + w, y_shift + h)
return dest
end
function data_augmentation.flip(src)
local flip = torch.random(1, 4)
local tr = torch.random(1, 2)
local src, conversion = iproc.byte2float(src)
local dest
src = src:contiguous()
if tr == 1 then
-- pass
elseif tr == 2 then
src = src:transpose(2, 3):contiguous()
end
if flip == 1 then
dest = image.hflip(src)
elseif flip == 2 then
dest = image.vflip(src)
elseif flip == 3 then
dest = image.hflip(image.vflip(src))
elseif flip == 4 then
dest = src
end
if conversion then
dest = iproc.float2byte(dest)
end
return dest
end
return data_augmentation

View file

@ -1,74 +1,108 @@
local gm = require 'graphicsmagick' local gm = require 'graphicsmagick'
local ffi = require 'ffi' local ffi = require 'ffi'
local iproc = require 'iproc'
require 'pl' require 'pl'
local image_loader = {} local image_loader = {}
function image_loader.decode_float(blob) local clip_eps8 = (1.0 / 255.0) * 0.5 - (1.0e-7 * (1.0 / 255.0) * 0.5)
local im, alpha = image_loader.decode_byte(blob) local clip_eps16 = (1.0 / 65535.0) * 0.5 - (1.0e-7 * (1.0 / 65535.0) * 0.5)
if im then local background_color = 0.5
im = im:float():div(255)
end function image_loader.encode_png(rgb, depth)
return im, alpha depth = depth or 8
end rgb = iproc.byte2float(rgb)
function image_loader.encode_png(rgb, alpha) if depth < 16 then
if rgb:type() == "torch.ByteTensor" then rgb = rgb:clone():add(clip_eps8)
error("expect FloatTensor") rgb[torch.lt(rgb, 0.0)] = 0.0
end rgb[torch.gt(rgb, 1.0)] = 1.0
if alpha then rgb = rgb:mul(255):long():float():div(255)
if not (alpha:size(2) == rgb:size(2) and alpha:size(3) == rgb:size(3)) then
alpha = gm.Image(alpha, "I", "DHW"):size(rgb:size(3), rgb:size(2), "Sinc"):toTensor("float", "I", "DHW")
end
local rgba = torch.Tensor(4, rgb:size(2), rgb:size(3))
rgba[1]:copy(rgb[1])
rgba[2]:copy(rgb[2])
rgba[3]:copy(rgb[3])
rgba[4]:copy(alpha)
local im = gm.Image():fromTensor(rgba, "RGBA", "DHW")
im:format("png")
return im:toBlob(9)
else else
local im = gm.Image(rgb, "RGB", "DHW") rgb = rgb:clone():add(clip_eps16)
im:format("png") rgb[torch.lt(rgb, 0.0)] = 0.0
return im:toBlob(9) rgb[torch.gt(rgb, 1.0)] = 1.0
rgb = rgb:mul(65535):long():float():div(65535)
end end
local im
if rgb:size(1) == 4 then -- RGBA
im = gm.Image(rgb, "RGBA", "DHW")
elseif rgb:size(1) == 3 then -- RGB
im = gm.Image(rgb, "RGB", "DHW")
elseif rgb:size(1) == 1 then -- Y
im = gm.Image(rgb, "I", "DHW")
-- im:colorspace("GRAY") -- it does not work
end
return im:depth(depth):format("PNG"):toString(9)
end end
function image_loader.save_png(filename, rgb, alpha) function image_loader.save_png(filename, rgb, depth)
local blob, len = image_loader.encode_png(rgb, alpha) depth = depth or 8
local blob = image_loader.encode_png(rgb, depth)
local fp = io.open(filename, "wb") local fp = io.open(filename, "wb")
fp:write(ffi.string(blob, len)) if not fp then
error("IO error: " .. filename)
end
fp:write(blob)
fp:close() fp:close()
return true return true
end end
function image_loader.decode_byte(blob) function image_loader.decode_float(blob)
local load_image = function() local load_image = function()
local im = gm.Image() local im = gm.Image()
local alpha = nil local alpha = nil
local gamma_lcd = 0.454545
im:fromBlob(blob, #blob) im:fromBlob(blob, #blob)
if im:colorspace() == "CMYK" then
im:colorspace("RGB")
end
local gamma = math.floor(im:gamma() * 1000000) / 1000000
if gamma ~= 0 and gamma ~= gamma_lcd then
local cg = gamma / gamma_lcd
im:gammaCorrection(cg, "Red")
im:gammaCorrection(cg, "Blue")
im:gammaCorrection(cg, "Green")
end
-- FIXME: How to detect that a image has an alpha channel? -- FIXME: How to detect that a image has an alpha channel?
if blob:sub(1, 4) == "\x89PNG" or blob:sub(1, 3) == "GIF" then if blob:sub(1, 4) == "\x89PNG" or blob:sub(1, 3) == "GIF" then
-- split alpha channel -- split alpha channel
im = im:toTensor('float', 'RGBA', 'DHW') im = im:toTensor('float', 'RGBA', 'DHW')
local sum_alpha = (im[4] - 1):sum() local sum_alpha = (im[4] - 1.0):sum()
if sum_alpha > 0 or sum_alpha < 0 then if sum_alpha < 0 then
alpha = im[4]:reshape(1, im:size(2), im:size(3)) alpha = im[4]:reshape(1, im:size(2), im:size(3))
-- drop full transparent background
local mask = torch.le(alpha, 0.0)
im[1][mask] = background_color
im[2][mask] = background_color
im[3][mask] = background_color
end end
local new_im = torch.FloatTensor(3, im:size(2), im:size(3)) local new_im = torch.FloatTensor(3, im:size(2), im:size(3))
new_im[1]:copy(im[1]) new_im[1]:copy(im[1])
new_im[2]:copy(im[2]) new_im[2]:copy(im[2])
new_im[3]:copy(im[3]) new_im[3]:copy(im[3])
im = new_im:mul(255):byte() im = new_im
else else
im = im:toTensor('byte', 'RGB', 'DHW') im = im:toTensor('float', 'RGB', 'DHW')
end end
return {im, alpha} return {im, alpha, blob}
end end
local state, ret = pcall(load_image) local state, ret = pcall(load_image)
if state then if state then
return ret[1], ret[2] return ret[1], ret[2], ret[3]
else else
return nil return nil, nil, nil
end
end
function image_loader.decode_byte(blob)
local im, alpha
im, alpha, blob = image_loader.decode_float(blob)
if im then
im = iproc.float2byte(im)
-- hmm, alpha does not convert here
return im, alpha, blob
else
return nil, nil, nil
end end
end end
function image_loader.load_float(file) function image_loader.load_float(file)
@ -90,18 +124,16 @@ function image_loader.load_byte(file)
return image_loader.decode_byte(buff) return image_loader.decode_byte(buff)
end end
local function test() local function test()
require 'image' torch.setdefaulttensortype("torch.FloatTensor")
local img local a = image_loader.load_float("../images/lena.png")
img = image_loader.load_float("./a.jpg") local blob = image_loader.encode_png(a)
if img then local b = image_loader.decode_float(blob)
print(img:min()) assert((b - a):abs():sum() == 0)
print(img:max())
image.display(img) a = image_loader.load_byte("../images/lena.png")
end blob = image_loader.encode_png(a)
img = image_loader.load_float("./b.png") b = image_loader.decode_byte(blob)
if img then assert((b:float() - a:float()):abs():sum() == 0)
image.display(img)
end
end end
--test() --test()
return image_loader return image_loader

View file

@ -1,16 +1,83 @@
local gm = require 'graphicsmagick' local gm = require 'graphicsmagick'
local image = require 'image' local image = require 'image'
local iproc = {}
function iproc.scale(src, width, height, filter) local iproc = {}
local t = "float" local clip_eps8 = (1.0 / 255.0) * 0.5 - (1.0e-7 * (1.0 / 255.0) * 0.5)
if src:type() == "torch.ByteTensor" then
t = "byte" function iproc.crop_mod4(src)
local w = src:size(3) % 4
local h = src:size(2) % 4
return iproc.crop(src, 0, 0, src:size(3) - w, src:size(2) - h)
end
function iproc.crop(src, w1, h1, w2, h2)
local dest
if src:dim() == 3 then
dest = src[{{}, { h1 + 1, h2 }, { w1 + 1, w2 }}]:clone()
else -- dim == 2
dest = src[{{ h1 + 1, h2 }, { w1 + 1, w2 }}]:clone()
end end
return dest
end
function iproc.crop_nocopy(src, w1, h1, w2, h2)
local dest
if src:dim() == 3 then
dest = src[{{}, { h1 + 1, h2 }, { w1 + 1, w2 }}]
else -- dim == 2
dest = src[{{ h1 + 1, h2 }, { w1 + 1, w2 }}]
end
return dest
end
function iproc.byte2float(src)
local conversion = false
local dest = src
if src:type() == "torch.ByteTensor" then
conversion = true
dest = src:float():div(255.0)
end
return dest, conversion
end
function iproc.float2byte(src)
local conversion = false
local dest = src
if src:type() == "torch.FloatTensor" then
conversion = true
dest = (src + clip_eps8):mul(255.0)
dest[torch.lt(dest, 0.0)] = 0
dest[torch.gt(dest, 255.0)] = 255.0
dest = dest:byte()
end
return dest, conversion
end
function iproc.scale(src, width, height, filter)
local conversion, color
src, conversion = iproc.byte2float(src)
filter = filter or "Box"
if src:size(1) == 3 then
color = "RGB"
else
color = "I"
end
local im = gm.Image(src, color, "DHW")
im:size(math.ceil(width), math.ceil(height), filter)
local dest = im:toTensor("float", color, "DHW")
if conversion then
dest = iproc.float2byte(dest)
end
return dest
end
function iproc.scale_with_gamma22(src, width, height, filter)
local conversion
src, conversion = iproc.byte2float(src)
filter = filter or "Box" filter = filter or "Box"
local im = gm.Image(src, "RGB", "DHW") local im = gm.Image(src, "RGB", "DHW")
im:size(math.ceil(width), math.ceil(height), filter) im:gammaCorrection(1.0 / 2.2):
return im:toTensor(t, "RGB", "DHW") size(math.ceil(width), math.ceil(height), filter):
gammaCorrection(2.2)
local dest = im:toTensor("float", "RGB", "DHW")
if conversion then
dest = iproc.float2byte(dest)
end
return dest
end end
function iproc.padding(img, w1, w2, h1, h2) function iproc.padding(img, w1, w2, h1, h2)
local dst_height = img:size(2) + h1 + h2 local dst_height = img:size(2) + h1 + h2
@ -22,5 +89,61 @@ function iproc.padding(img, w1, w2, h1, h2)
flow[2]:add(-w1) flow[2]:add(-w1)
return image.warp(img, flow, "simple", false, "clamp") return image.warp(img, flow, "simple", false, "clamp")
end end
function iproc.zero_padding(img, w1, w2, h1, h2)
local dst_height = img:size(2) + h1 + h2
local dst_width = img:size(3) + w1 + w2
local flow = torch.Tensor(2, dst_height, dst_width)
flow[1] = torch.ger(torch.linspace(0, dst_height -1, dst_height), torch.ones(dst_width))
flow[2] = torch.ger(torch.ones(dst_height), torch.linspace(0, dst_width - 1, dst_width))
flow[1]:add(-h1)
flow[2]:add(-w1)
return image.warp(img, flow, "simple", false, "pad", 0)
end
function iproc.white_noise(src, std, rgb_weights, gamma)
gamma = gamma or 0.454545
local conversion
src, conversion = iproc.byte2float(src)
std = std or 0.01
local noise = torch.Tensor():resizeAs(src):normal(0, std)
if rgb_weights then
noise[1]:mul(rgb_weights[1])
noise[2]:mul(rgb_weights[2])
noise[3]:mul(rgb_weights[3])
end
local dest
if gamma ~= 0 then
dest = src:clone():pow(gamma):add(noise)
dest[torch.lt(dest, 0.0)] = 0.0
dest[torch.gt(dest, 1.0)] = 1.0
dest:pow(1.0 / gamma)
else
dest = src + noise
end
if conversion then
dest = iproc.float2byte(dest)
end
return dest
end
local function test_conversion()
local a = torch.linspace(0, 255, 256):float():div(255.0)
local b = iproc.float2byte(a)
local c = iproc.byte2float(a)
local d = torch.linspace(0, 255, 256)
assert((a - c):abs():sum() == 0)
assert((d:float() - b:float()):abs():sum() == 0)
a = torch.FloatTensor({256.0, 255.0, 254.999}):div(255.0)
b = iproc.float2byte(a)
assert(b:float():sum() == 255.0 * 3)
a = torch.FloatTensor({254.0, 254.499, 253.50001}):div(255.0)
b = iproc.float2byte(a)
print(b)
assert(b:float():sum() == 254.0 * 3)
end
--test_conversion()
return iproc return iproc

View file

@ -3,38 +3,35 @@ require 'cutorch'
require 'xlua' require 'xlua'
local function minibatch_adam(model, criterion, local function minibatch_adam(model, criterion,
train_x, train_x, train_y,
config, transformer, config)
input_size, target_size)
local parameters, gradParameters = model:getParameters() local parameters, gradParameters = model:getParameters()
config = config or {} config = config or {}
local sum_loss = 0 local sum_loss = 0
local count_loss = 0 local count_loss = 0
local batch_size = config.xBatchSize or 32 local batch_size = config.xBatchSize or 32
local shuffle = torch.randperm(#train_x) local shuffle = torch.randperm(train_x:size(1))
local c = 1 local c = 1
local inputs = torch.Tensor(batch_size,
input_size[1], input_size[2], input_size[3]):cuda()
local targets = torch.Tensor(batch_size,
target_size[1] * target_size[2] * target_size[3]):cuda()
local inputs_tmp = torch.Tensor(batch_size, local inputs_tmp = torch.Tensor(batch_size,
input_size[1], input_size[2], input_size[3]) train_x:size(2), train_x:size(3), train_x:size(4)):zero()
local targets_tmp = torch.Tensor(batch_size, local targets_tmp = torch.Tensor(batch_size,
target_size[1] * target_size[2] * target_size[3]) train_y:size(2)):zero()
local inputs = inputs_tmp:clone():cuda()
for t = 1, #train_x, batch_size do local targets = targets_tmp:clone():cuda()
if t + batch_size > #train_x then
print("## update")
for t = 1, train_x:size(1), batch_size do
if t + batch_size -1 > train_x:size(1) then
break break
end end
xlua.progress(t, #train_x) xlua.progress(t, train_x:size(1))
for i = 1, batch_size do for i = 1, batch_size do
local x, y = transformer(train_x[shuffle[t + i - 1]]) inputs_tmp[i]:copy(train_x[shuffle[t + i - 1]])
inputs_tmp[i]:copy(x) targets_tmp[i]:copy(train_y[shuffle[t + i - 1]])
targets_tmp[i]:copy(y)
end end
inputs:copy(inputs_tmp) inputs:copy(inputs_tmp)
targets:copy(targets_tmp) targets:copy(targets_tmp)
local feval = function(x) local feval = function(x)
if x ~= parameters then if x ~= parameters then
parameters:copy(x) parameters:copy(x)
@ -48,15 +45,14 @@ local function minibatch_adam(model, criterion,
return f, gradParameters return f, gradParameters
end end
optim.adam(feval, parameters, config) optim.adam(feval, parameters, config)
c = c + 1 c = c + 1
if c % 10 == 0 then if c % 50 == 0 then
collectgarbage() collectgarbage()
end end
end end
xlua.progress(#train_x, #train_x) xlua.progress(train_x:size(1), train_x:size(1))
return { mse = sum_loss / count_loss} return { loss = sum_loss / count_loss}
end end
return minibatch_adam return minibatch_adam

View file

@ -1,291 +1,263 @@
require 'image' require 'image'
local gm = require 'graphicsmagick' local gm = require 'graphicsmagick'
local iproc = require './iproc' local iproc = require 'iproc'
local reconstruct = require './reconstruct' local data_augmentation = require 'data_augmentation'
local pairwise_transform = {} local pairwise_transform = {}
local function random_half(src, p, min_size) local function random_half(src, p)
p = p or 0.5 if torch.uniform() < p then
local filter = ({"Box","Blackman", "SincFast", "Jinc"})[torch.random(1, 4)] local filter = ({"Box","Box","Blackman","Sinc","Lanczos", "Catrom"})[torch.random(1, 6)]
if p > torch.uniform() then
return iproc.scale(src, src:size(3) * 0.5, src:size(2) * 0.5, filter) return iproc.scale(src, src:size(3) * 0.5, src:size(2) * 0.5, filter)
else else
return src return src
end end
end end
local function color_augment(x) local function crop_if_large(src, max_size)
local color_scale = torch.Tensor(3):uniform(0.8, 1.2) local tries = 4
x = x:float():div(255) if src:size(2) > max_size and src:size(3) > max_size then
for i = 1, 3 do local rect
x[i]:mul(color_scale[i]) for i = 1, tries do
end local yi = torch.random(0, src:size(2) - max_size)
x[torch.lt(x, 0.0)] = 0.0 local xi = torch.random(0, src:size(3) - max_size)
x[torch.gt(x, 1.0)] = 1.0 rect = iproc.crop(src, xi, yi, xi + max_size, yi + max_size)
return x:mul(255):byte() -- ignore simple background
end if rect:float():std() >= 0 then
local function flip_augment(x, y) break
local flip = torch.random(1, 4) end
if y then
if flip == 1 then
x = image.hflip(x)
y = image.hflip(y)
elseif flip == 2 then
x = image.vflip(x)
y = image.vflip(y)
elseif flip == 3 then
x = image.hflip(image.vflip(x))
y = image.hflip(image.vflip(y))
elseif flip == 4 then
end end
return x, y return rect
else else
if flip == 1 then return src
x = image.hflip(x)
elseif flip == 2 then
x = image.vflip(x)
elseif flip == 3 then
x = image.hflip(image.vflip(x))
elseif flip == 4 then
end
return x
end end
end end
local INTERPOLATION_PADDING = 16 local function preprocess(src, crop_size, options)
function pairwise_transform.scale(src, scale, size, offset, options) local dest = src
options = options or {color_augment = true, random_half = true, rgb = true} dest = random_half(dest, options.random_half_rate)
if options.random_half then dest = crop_if_large(dest, math.max(crop_size * 2, options.max_size))
src = random_half(src) dest = data_augmentation.flip(dest)
dest = data_augmentation.color_noise(dest, options.random_color_noise_rate)
dest = data_augmentation.overlay(dest, options.random_overlay_rate)
dest = data_augmentation.unsharp_mask(dest, options.random_unsharp_mask_rate)
dest = data_augmentation.shift_1px(dest)
return dest
end
local function active_cropping(x, y, size, p, tries)
assert("x:size == y:size", x:size(2) == y:size(2) and x:size(3) == y:size(3))
local r = torch.uniform()
local t = "float"
if x:type() == "torch.ByteTensor" then
t = "byte"
end end
local yi = torch.random(INTERPOLATION_PADDING, src:size(2) - size - INTERPOLATION_PADDING) if p < r then
local xi = torch.random(INTERPOLATION_PADDING, src:size(3) - size - INTERPOLATION_PADDING) local xi = torch.random(0, y:size(3) - (size + 1))
local down_scale = 1.0 / scale local yi = torch.random(0, y:size(2) - (size + 1))
local y = image.crop(src, local xc = iproc.crop(x, xi, yi, xi + size, yi + size)
xi - INTERPOLATION_PADDING, yi - INTERPOLATION_PADDING, local yc = iproc.crop(y, xi, yi, xi + size, yi + size)
xi + size + INTERPOLATION_PADDING, yi + size + INTERPOLATION_PADDING) return xc, yc
local filters = { else
"Box", -- 0.012756949974688 local lowres = gm.Image(x, "RGB", "DHW"):
"Blackman", -- 0.013191924552285 size(x:size(3) * 0.5, x:size(2) * 0.5, "Box"):
--"Cartom", -- 0.013753536746706 size(x:size(3), x:size(2), "Box"):
--"Hanning", -- 0.013761314529647 toTensor(t, "RGB", "DHW")
--"Hermite", -- 0.013850225205266 local best_se = 0.0
"SincFast", -- 0.014095824314306 local best_xc, best_yc
"Jinc", -- 0.014244299255442 local m = torch.FloatTensor(x:size(1), size, size)
} for i = 1, tries do
local xi = torch.random(0, y:size(3) - (size + 1))
local yi = torch.random(0, y:size(2) - (size + 1))
local xc = iproc.crop(x, xi, yi, xi + size, yi + size)
local lc = iproc.crop(lowres, xi, yi, xi + size, yi + size)
local xcf = iproc.byte2float(xc)
local lcf = iproc.byte2float(lc)
local se = m:copy(xcf):add(-1.0, lcf):pow(2):sum()
if se >= best_se then
best_xc = xcf
best_yc = iproc.byte2float(iproc.crop(y, xi, yi, xi + size, yi + size))
best_se = se
end
end
return best_xc, best_yc
end
end
function pairwise_transform.scale(src, scale, size, offset, n, options)
local filters;
if options.style == "photo" then
filters = {
"Box", "lanczos", "Catrom"
}
else
filters = {
"Box","Box", -- 0.012756949974688
"Blackman", -- 0.013191924552285
--"Catrom", -- 0.013753536746706
--"Hanning", -- 0.013761314529647
--"Hermite", -- 0.013850225205266
"Sinc", -- 0.014095824314306
"Lanczos", -- 0.014244299255442
}
end
local unstable_region_offset = 8
local downscale_filter = filters[torch.random(1, #filters)] local downscale_filter = filters[torch.random(1, #filters)]
local y = preprocess(src, size, options)
assert(y:size(2) % 4 == 0 and y:size(3) % 4 == 0)
local down_scale = 1.0 / scale
local x = iproc.scale(iproc.scale(y, y:size(3) * down_scale,
y:size(2) * down_scale, downscale_filter),
y:size(3), y:size(2))
x = iproc.crop(x, unstable_region_offset, unstable_region_offset,
x:size(3) - unstable_region_offset, x:size(2) - unstable_region_offset)
y = iproc.crop(y, unstable_region_offset, unstable_region_offset,
y:size(3) - unstable_region_offset, y:size(2) - unstable_region_offset)
assert(x:size(2) % 4 == 0 and x:size(3) % 4 == 0)
assert(x:size(1) == y:size(1) and x:size(2) == y:size(2) and x:size(3) == y:size(3))
y = flip_augment(y) local batch = {}
if options.color_augment then for i = 1, n do
y = color_augment(y) local xc, yc = active_cropping(x, y,
size,
options.active_cropping_rate,
options.active_cropping_tries)
xc = iproc.byte2float(xc)
yc = iproc.byte2float(yc)
if options.rgb then
else
yc = image.rgb2yuv(yc)[1]:reshape(1, yc:size(2), yc:size(3))
xc = image.rgb2yuv(xc)[1]:reshape(1, xc:size(2), xc:size(3))
end
table.insert(batch, {xc, iproc.crop(yc, offset, offset, size - offset, size - offset)})
end end
local x = iproc.scale(y, y:size(3) * down_scale, y:size(2) * down_scale, downscale_filter) return batch
x = iproc.scale(x, y:size(3), y:size(2))
y = y:float():div(255)
x = x:float():div(255)
if options.rgb then
else
y = image.rgb2yuv(y)[1]:reshape(1, y:size(2), y:size(3))
x = image.rgb2yuv(x)[1]:reshape(1, x:size(2), x:size(3))
end
y = image.crop(y, INTERPOLATION_PADDING + offset, INTERPOLATION_PADDING + offset, y:size(3) - offset - INTERPOLATION_PADDING, y:size(2) - offset - INTERPOLATION_PADDING)
x = image.crop(x, INTERPOLATION_PADDING, INTERPOLATION_PADDING, x:size(3) - INTERPOLATION_PADDING, x:size(2) - INTERPOLATION_PADDING)
return x, y
end end
function pairwise_transform.jpeg_(src, quality, size, offset, options) function pairwise_transform.jpeg_(src, quality, size, offset, n, options)
options = options or {color_augment = true, random_half = true, rgb = true} local unstable_region_offset = 8
if options.random_half then local y = preprocess(src, size, options)
src = random_half(src) local x = y
end
local yi = torch.random(0, src:size(2) - size - 1)
local xi = torch.random(0, src:size(3) - size - 1)
local y = src
local x
if options.color_augment then
y = color_augment(y)
end
x = y
for i = 1, #quality do for i = 1, #quality do
x = gm.Image(x, "RGB", "DHW") x = gm.Image(x, "RGB", "DHW")
x:format("jpeg") x:format("jpeg"):depth(8)
x:samplingFactors({1.0, 1.0, 1.0}) if torch.uniform() < options.jpeg_chroma_subsampling_rate then
-- YUV 420
x:samplingFactors({2.0, 1.0, 1.0})
else
-- YUV 444
x:samplingFactors({1.0, 1.0, 1.0})
end
local blob, len = x:toBlob(quality[i]) local blob, len = x:toBlob(quality[i])
x:fromBlob(blob, len) x:fromBlob(blob, len)
x = x:toTensor("byte", "RGB", "DHW") x = x:toTensor("byte", "RGB", "DHW")
end end
x = iproc.crop(x, unstable_region_offset, unstable_region_offset,
x:size(3) - unstable_region_offset, x:size(2) - unstable_region_offset)
y = iproc.crop(y, unstable_region_offset, unstable_region_offset,
y:size(3) - unstable_region_offset, y:size(2) - unstable_region_offset)
assert(x:size(2) % 4 == 0 and x:size(3) % 4 == 0)
assert(x:size(1) == y:size(1) and x:size(2) == y:size(2) and x:size(3) == y:size(3))
y = image.crop(y, xi, yi, xi + size, yi + size) local batch = {}
x = image.crop(x, xi, yi, xi + size, yi + size) for i = 1, n do
y = y:float():div(255) local xc, yc = active_cropping(x, y, size,
x = x:float():div(255) options.active_cropping_rate,
x, y = flip_augment(x, y) options.active_cropping_tries)
xc = iproc.byte2float(xc)
if options.rgb then yc = iproc.byte2float(yc)
else if options.rgb then
y = image.rgb2yuv(y)[1]:reshape(1, y:size(2), y:size(3)) else
x = image.rgb2yuv(x)[1]:reshape(1, x:size(2), x:size(3)) yc = image.rgb2yuv(yc)[1]:reshape(1, yc:size(2), yc:size(3))
xc = image.rgb2yuv(xc)[1]:reshape(1, xc:size(2), xc:size(3))
end
if torch.uniform() < options.nr_rate then
-- reducing noise
table.insert(batch, {xc, iproc.crop(yc, offset, offset, size - offset, size - offset)})
else
-- ratain useful details
table.insert(batch, {yc, iproc.crop(yc, offset, offset, size - offset, size - offset)})
end
end end
return batch
return x, image.crop(y, offset, offset, size - offset, size - offset)
end end
function pairwise_transform.jpeg(src, level, size, offset, options) function pairwise_transform.jpeg(src, style, level, size, offset, n, options)
if level == 1 then if style == "art" then
return pairwise_transform.jpeg_(src, {torch.random(65, 85)}, if level == 1 then
size, offset, return pairwise_transform.jpeg_(src, {torch.random(65, 85)},
size, offset, n, options)
elseif level == 2 then
local r = torch.uniform()
if r > 0.6 then
return pairwise_transform.jpeg_(src, {torch.random(27, 70)},
size, offset, n, options)
elseif r > 0.3 then
local quality1 = torch.random(37, 70)
local quality2 = quality1 - torch.random(5, 10)
return pairwise_transform.jpeg_(src, {quality1, quality2},
size, offset, n, options)
else
local quality1 = torch.random(52, 70)
local quality2 = quality1 - torch.random(5, 15)
local quality3 = quality1 - torch.random(15, 25)
return pairwise_transform.jpeg_(src,
{quality1, quality2, quality3},
size, offset, n, options)
end
else
error("unknown noise level: " .. level)
end
elseif style == "photo" then
-- level adjusting by -nr_rate
return pairwise_transform.jpeg_(src, {torch.random(30, 70)},
size, offset, n,
options) options)
elseif level == 2 then
local r = torch.uniform()
if r > 0.6 then
return pairwise_transform.jpeg_(src, {torch.random(27, 70)},
size, offset,
options)
elseif r > 0.3 then
local quality1 = torch.random(37, 70)
local quality2 = quality1 - torch.random(5, 10)
return pairwise_transform.jpeg_(src, {quality1, quality2},
size, offset,
options)
else
local quality1 = torch.random(52, 70)
return pairwise_transform.jpeg_(src,
{quality1,
quality1 - torch.random(5, 15),
quality1 - torch.random(15, 25)},
size, offset,
options)
end
else else
error("unknown noise level: " .. level) error("unknown style: " .. style)
end end
end end
function pairwise_transform.jpeg_scale_(src, scale, quality, size, offset, options)
if options.random_half then function pairwise_transform.test_jpeg(src)
src = random_half(src) torch.setdefaulttensortype("torch.FloatTensor")
end local options = {random_color_noise_rate = 0.5,
local down_scale = 1.0 / scale random_half_rate = 0.5,
local filters = { random_overlay_rate = 0.5,
"Box", -- 0.012756949974688 random_unsharp_mask_rate = 0.5,
"Blackman", -- 0.013191924552285 jpeg_chroma_subsampling_rate = 0.5,
--"Cartom", -- 0.013753536746706 nr_rate = 1.0,
--"Hanning", -- 0.013761314529647 active_cropping_rate = 0.5,
--"Hermite", -- 0.013850225205266 active_cropping_tries = 10,
"SincFast", -- 0.014095824314306 max_size = 256,
"Jinc", -- 0.014244299255442 rgb = true
} }
local downscale_filter = filters[torch.random(1, #filters)] local image = require 'image'
local yi = torch.random(INTERPOLATION_PADDING, src:size(2) - size - INTERPOLATION_PADDING) local src = image.lena()
local xi = torch.random(INTERPOLATION_PADDING, src:size(3) - size - INTERPOLATION_PADDING)
local y = src
local x
if options.color_augment then
y = color_augment(y)
end
x = y
x = iproc.scale(x, y:size(3) * down_scale, y:size(2) * down_scale, downscale_filter)
for i = 1, #quality do
x = gm.Image(x, "RGB", "DHW")
x:format("jpeg")
x:samplingFactors({1.0, 1.0, 1.0})
local blob, len = x:toBlob(quality[i])
x:fromBlob(blob, len)
x = x:toTensor("byte", "RGB", "DHW")
end
x = iproc.scale(x, y:size(3), y:size(2))
y = image.crop(y,
xi, yi,
xi + size, yi + size)
x = image.crop(x,
xi, yi,
xi + size, yi + size)
x = x:float():div(255)
y = y:float():div(255)
x, y = flip_augment(x, y)
if options.rgb then
else
y = image.rgb2yuv(y)[1]:reshape(1, y:size(2), y:size(3))
x = image.rgb2yuv(x)[1]:reshape(1, x:size(2), x:size(3))
end
return x, image.crop(y, offset, offset, size - offset, size - offset)
end
function pairwise_transform.jpeg_scale(src, scale, level, size, offset, options)
options = options or {color_augment = true, random_half = true}
if level == 1 then
return pairwise_transform.jpeg_scale_(src, scale, {torch.random(65, 85)},
size, offset, options)
elseif level == 2 then
local r = torch.uniform()
if r > 0.6 then
return pairwise_transform.jpeg_scale_(src, scale, {torch.random(27, 70)},
size, offset, options)
elseif r > 0.3 then
local quality1 = torch.random(37, 70)
local quality2 = quality1 - torch.random(5, 10)
return pairwise_transform.jpeg_scale_(src, scale, {quality1, quality2},
size, offset, options)
else
local quality1 = torch.random(52, 70)
return pairwise_transform.jpeg_scale_(src, scale,
{quality1,
quality1 - torch.random(5, 15),
quality1 - torch.random(15, 25)},
size, offset, options)
end
else
error("unknown noise level: " .. level)
end
end
local function test_jpeg()
local loader = require './image_loader'
local src = loader.load_byte("../images/miku_CC_BY-NC.jpg")
local y, x = pairwise_transform.jpeg_(src, {}, 128, 0, false)
image.display({image = y, legend = "y:0"})
image.display({image = x, legend = "x:0"})
for i = 2, 9 do
local y, x = pairwise_transform.jpeg_(pairwise_transform.random_half(src),
{i * 10}, 128, 0, {color_augment = false, random_half = true})
image.display({image = y, legend = "y:" .. (i * 10), max=1,min=0})
image.display({image = x, legend = "x:" .. (i * 10),max=1,min=0})
--print(x:mean(), y:mean())
end
end
local function test_scale()
local loader = require './image_loader'
local src = loader.load_byte("../images/miku_CC_BY-NC.jpg")
for i = 1, 9 do for i = 1, 9 do
local y, x = pairwise_transform.scale(src, 2.0, 128, 7, {color_augment = true, random_half = true, rgb = true}) local xy = pairwise_transform.jpeg(src,
image.display({image = y, legend = "y:" .. (i * 10), min = 0, max = 1}) "art",
image.display({image = x, legend = "x:" .. (i * 10), min = 0, max = 1}) torch.random(1, 2),
print(y:size(), x:size()) 128, 7, 1, options)
--print(x:mean(), y:mean()) image.display({image = xy[1][1], legend = "y:" .. (i * 10), min=0, max=1})
image.display({image = xy[1][2], legend = "x:" .. (i * 10), min=0, max=1})
end end
end end
local function test_jpeg_scale() function pairwise_transform.test_scale(src)
local loader = require './image_loader' torch.setdefaulttensortype("torch.FloatTensor")
local src = loader.load_byte("../images/miku_CC_BY-NC.jpg") local options = {random_color_noise_rate = 0.5,
for i = 1, 9 do random_half_rate = 0.5,
local y, x = pairwise_transform.jpeg_scale(src, 2.0, 1, 128, 7, {color_augment = true, random_half = true}) random_overlay_rate = 0.5,
image.display({image = y, legend = "y1:" .. (i * 10), min = 0, max = 1}) random_unsharp_mask_rate = 0.5,
image.display({image = x, legend = "x1:" .. (i * 10), min = 0, max = 1}) active_cropping_rate = 0.5,
print(y:size(), x:size()) active_cropping_tries = 10,
--print(x:mean(), y:mean()) max_size = 256,
end rgb = true
for i = 1, 9 do }
local y, x = pairwise_transform.jpeg_scale(src, 2.0, 2, 128, 7, {color_augment = true, random_half = true}) local image = require 'image'
image.display({image = y, legend = "y2:" .. (i * 10), min = 0, max = 1}) local src = image.lena()
image.display({image = x, legend = "x2:" .. (i * 10), min = 0, max = 1})
print(y:size(), x:size())
--print(x:mean(), y:mean())
end
end
--test_scale()
--test_jpeg()
--test_jpeg_scale()
for i = 1, 10 do
local xy = pairwise_transform.scale(src, 2.0, 128, 7, 1, options)
image.display({image = xy[1][1], legend = "y:" .. (i * 10), min = 0, max = 1})
image.display({image = xy[1][2], legend = "x:" .. (i * 10), min = 0, max = 1})
end
end
return pairwise_transform return pairwise_transform

View file

@ -1,4 +0,0 @@
require 'torch'
require 'cutorch'
require 'nn'
require 'cunn'

View file

@ -1,5 +1,5 @@
require 'image' require 'image'
local iproc = require './iproc' local iproc = require 'iproc'
local function reconstruct_y(model, x, offset, block_size) local function reconstruct_y(model, x, offset, block_size)
if x:dim() == 2 then if x:dim() == 2 then
@ -48,7 +48,8 @@ local function reconstruct_rgb(model, x, offset, block_size)
end end
return new_x return new_x
end end
function model_is_rgb(model) local reconstruct = {}
function reconstruct.is_rgb(model)
if model:get(model:size() - 1).weight:size(1) == 3 then if model:get(model:size() - 1).weight:size(1) == 3 then
-- 3ch RGB -- 3ch RGB
return true return true
@ -57,8 +58,23 @@ function model_is_rgb(model)
return false return false
end end
end end
function reconstruct.offset_size(model)
local reconstruct = {} local conv = model:findModules("nn.SpatialConvolutionMM")
if #conv > 0 then
local offset = 0
for i = 1, #conv do
offset = offset + (conv[i].kW - 1) / 2
end
return math.floor(offset)
else
conv = model:findModules("cudnn.SpatialConvolution")
local offset = 0
for i = 1, #conv do
offset = offset + (conv[i].kW - 1) / 2
end
return math.floor(offset)
end
end
function reconstruct.image_y(model, x, offset, block_size) function reconstruct.image_y(model, x, offset, block_size)
block_size = block_size or 128 block_size = block_size or 128
local output_size = block_size - offset * 2 local output_size = block_size - offset * 2
@ -78,7 +94,7 @@ function reconstruct.image_y(model, x, offset, block_size)
y[torch.lt(y, 0)] = 0 y[torch.lt(y, 0)] = 0
y[torch.gt(y, 1)] = 1 y[torch.gt(y, 1)] = 1
yuv[1]:copy(y) yuv[1]:copy(y)
local output = image.yuv2rgb(image.crop(yuv, local output = image.yuv2rgb(iproc.crop(yuv,
pad_w1, pad_h1, pad_w1, pad_h1,
yuv:size(3) - pad_w2, yuv:size(2) - pad_h2)) yuv:size(3) - pad_w2, yuv:size(2) - pad_h2))
output[torch.lt(output, 0)] = 0 output[torch.lt(output, 0)] = 0
@ -89,7 +105,7 @@ function reconstruct.image_y(model, x, offset, block_size)
end end
function reconstruct.scale_y(model, scale, x, offset, block_size) function reconstruct.scale_y(model, scale, x, offset, block_size)
block_size = block_size or 128 block_size = block_size or 128
local x_jinc = iproc.scale(x, x:size(3) * scale, x:size(2) * scale, "Jinc") local x_lanczos = iproc.scale(x, x:size(3) * scale, x:size(2) * scale, "Lanczos")
x = iproc.scale(x, x:size(3) * scale, x:size(2) * scale, "Box") x = iproc.scale(x, x:size(3) * scale, x:size(2) * scale, "Box")
local output_size = block_size - offset * 2 local output_size = block_size - offset * 2
@ -105,14 +121,14 @@ function reconstruct.scale_y(model, scale, x, offset, block_size)
local pad_h2 = (h - offset) - x:size(2) local pad_h2 = (h - offset) - x:size(2)
local pad_w2 = (w - offset) - x:size(3) local pad_w2 = (w - offset) - x:size(3)
local yuv_nn = image.rgb2yuv(iproc.padding(x, pad_w1, pad_w2, pad_h1, pad_h2)) local yuv_nn = image.rgb2yuv(iproc.padding(x, pad_w1, pad_w2, pad_h1, pad_h2))
local yuv_jinc = image.rgb2yuv(iproc.padding(x_jinc, pad_w1, pad_w2, pad_h1, pad_h2)) local yuv_lanczos = image.rgb2yuv(iproc.padding(x_lanczos, pad_w1, pad_w2, pad_h1, pad_h2))
local y = reconstruct_y(model, yuv_nn[1], offset, block_size) local y = reconstruct_y(model, yuv_nn[1], offset, block_size)
y[torch.lt(y, 0)] = 0 y[torch.lt(y, 0)] = 0
y[torch.gt(y, 1)] = 1 y[torch.gt(y, 1)] = 1
yuv_jinc[1]:copy(y) yuv_lanczos[1]:copy(y)
local output = image.yuv2rgb(image.crop(yuv_jinc, local output = image.yuv2rgb(iproc.crop(yuv_lanczos,
pad_w1, pad_h1, pad_w1, pad_h1,
yuv_jinc:size(3) - pad_w2, yuv_jinc:size(2) - pad_h2)) yuv_lanczos:size(3) - pad_w2, yuv_lanczos:size(2) - pad_h2))
output[torch.lt(output, 0)] = 0 output[torch.lt(output, 0)] = 0
output[torch.gt(output, 1)] = 1 output[torch.gt(output, 1)] = 1
collectgarbage() collectgarbage()
@ -135,7 +151,7 @@ function reconstruct.image_rgb(model, x, offset, block_size)
local pad_w2 = (w - offset) - x:size(3) local pad_w2 = (w - offset) - x:size(3)
local input = iproc.padding(x, pad_w1, pad_w2, pad_h1, pad_h2) local input = iproc.padding(x, pad_w1, pad_w2, pad_h1, pad_h2)
local y = reconstruct_rgb(model, input, offset, block_size) local y = reconstruct_rgb(model, input, offset, block_size)
local output = image.crop(y, local output = iproc.crop(y,
pad_w1, pad_h1, pad_w1, pad_h1,
y:size(3) - pad_w2, y:size(2) - pad_h2) y:size(3) - pad_w2, y:size(2) - pad_h2)
collectgarbage() collectgarbage()
@ -162,7 +178,7 @@ function reconstruct.scale_rgb(model, scale, x, offset, block_size)
local pad_w2 = (w - offset) - x:size(3) local pad_w2 = (w - offset) - x:size(3)
local input = iproc.padding(x, pad_w1, pad_w2, pad_h1, pad_h2) local input = iproc.padding(x, pad_w1, pad_w2, pad_h1, pad_h2)
local y = reconstruct_rgb(model, input, offset, block_size) local y = reconstruct_rgb(model, input, offset, block_size)
local output = image.crop(y, local output = iproc.crop(y,
pad_w1, pad_h1, pad_w1, pad_h1,
y:size(3) - pad_w2, y:size(2) - pad_h2) y:size(3) - pad_w2, y:size(2) - pad_h2)
output[torch.lt(output, 0)] = 0 output[torch.lt(output, 0)] = 0
@ -172,18 +188,107 @@ function reconstruct.scale_rgb(model, scale, x, offset, block_size)
return output return output
end end
function reconstruct.image(model, x, offset, block_size) function reconstruct.image(model, x, block_size)
if model_is_rgb(model) then local i2rgb = false
return reconstruct.image_rgb(model, x, offset, block_size) if x:size(1) == 1 then
local new_x = torch.Tensor(3, x:size(2), x:size(3))
new_x[1]:copy(x)
new_x[2]:copy(x)
new_x[3]:copy(x)
x = new_x
i2rgb = true
end
if reconstruct.is_rgb(model) then
x = reconstruct.image_rgb(model, x,
reconstruct.offset_size(model), block_size)
else else
return reconstruct.image_y(model, x, offset, block_size) x = reconstruct.image_y(model, x,
reconstruct.offset_size(model), block_size)
end
if i2rgb then
x = image.rgb2y(x)
end
return x
end
function reconstruct.scale(model, scale, x, block_size)
local i2rgb = false
if x:size(1) == 1 then
local new_x = torch.Tensor(3, x:size(2), x:size(3))
new_x[1]:copy(x)
new_x[2]:copy(x)
new_x[3]:copy(x)
x = new_x
i2rgb = true
end
if reconstruct.is_rgb(model) then
x = reconstruct.scale_rgb(model, scale, x,
reconstruct.offset_size(model), block_size)
else
x = reconstruct.scale_y(model, scale, x,
reconstruct.offset_size(model), block_size)
end
if i2rgb then
x = image.rgb2y(x)
end
return x
end
local function tta(f, model, x, block_size)
local average = nil
local offset = reconstruct.offset_size(model)
for i = 1, 4 do
local flip_f, iflip_f
if i == 1 then
flip_f = function (a) return a end
iflip_f = function (a) return a end
elseif i == 2 then
flip_f = image.vflip
iflip_f = image.vflip
elseif i == 3 then
flip_f = image.hflip
iflip_f = image.hflip
elseif i == 4 then
flip_f = function (a) return image.hflip(image.vflip(a)) end
iflip_f = function (a) return image.vflip(image.hflip(a)) end
end
for j = 1, 2 do
local tr_f, itr_f
if j == 1 then
tr_f = function (a) return a end
itr_f = function (a) return a end
elseif j == 2 then
tr_f = function(a) return a:transpose(2, 3):contiguous() end
itr_f = function(a) return a:transpose(2, 3):contiguous() end
end
local out = itr_f(iflip_f(f(model, flip_f(tr_f(x)),
offset, block_size)))
if not average then
average = out
else
average:add(out)
end
end
end
return average:div(8.0)
end
function reconstruct.image_tta(model, x, block_size)
if reconstruct.is_rgb(model) then
return tta(reconstruct.image_rgb, model, x, block_size)
else
return tta(reconstruct.image_y, model, x, block_size)
end end
end end
function reconstruct.scale(model, scale, x, offset, block_size) function reconstruct.scale_tta(model, scale, x, block_size)
if model_is_rgb(model) then if reconstruct.is_rgb(model) then
return reconstruct.scale_rgb(model, scale, x, offset, block_size) local f = function (model, x, offset, block_size)
return reconstruct.scale_rgb(model, scale, x, offset, block_size)
end
return tta(f, model, x, block_size)
else else
return reconstruct.scale_y(model, scale, x, offset, block_size) local f = function (model, x, offset, block_size)
return reconstruct.scale_y(model, scale, x, offset, block_size)
end
return tta(f, model, x, block_size)
end end
end end

View file

@ -1,5 +1,6 @@
require 'xlua' require 'xlua'
require 'pl' require 'pl'
require 'trepl'
-- global settings -- global settings
@ -14,38 +15,68 @@ local settings = {}
local cmd = torch.CmdLine() local cmd = torch.CmdLine()
cmd:text() cmd:text()
cmd:text("waifu2x") cmd:text("waifu2x-training")
cmd:text("Options:") cmd:text("Options:")
cmd:option("-seed", 11, 'fixed input seed') cmd:option("-gpu", -1, 'GPU Device ID')
cmd:option("-data_dir", "./data", 'data directory') cmd:option("-seed", 11, 'RNG seed')
cmd:option("-test", "images/miku_small.png", 'test image file') cmd:option("-data_dir", "./data", 'path to data directory')
cmd:option("-backend", "cunn", '(cunn|cudnn)')
cmd:option("-test", "images/miku_small.png", 'path to test image')
cmd:option("-model_dir", "./models", 'model directory') cmd:option("-model_dir", "./models", 'model directory')
cmd:option("-method", "scale", '(noise|scale|noise_scale)') cmd:option("-method", "scale", 'method to training (noise|scale)')
cmd:option("-noise_level", 1, '(1|2)') cmd:option("-noise_level", 1, '(1|2)')
cmd:option("-style", "art", '(art|photo)')
cmd:option("-color", 'rgb', '(y|rgb)') cmd:option("-color", 'rgb', '(y|rgb)')
cmd:option("-scale", 2.0, 'scale') cmd:option("-random_color_noise_rate", 0.0, 'data augmentation using color noise (0.0-1.0)')
cmd:option("-learning_rate", 0.00025, 'learning rate for adam') cmd:option("-random_overlay_rate", 0.0, 'data augmentation using flipped image overlay (0.0-1.0)')
cmd:option("-random_half", 1, 'enable data augmentation using half resolution image') cmd:option("-random_half_rate", 0.0, 'data augmentation using half resolution image (0.0-1.0)')
cmd:option("-crop_size", 128, 'crop size') cmd:option("-random_unsharp_mask_rate", 0.0, 'data augmentation using unsharp mask (0.0-1.0)')
cmd:option("-batch_size", 2, 'mini batch size') cmd:option("-scale", 2.0, 'scale factor (2)')
cmd:option("-epoch", 200, 'epoch') cmd:option("-learning_rate", 0.0005, 'learning rate for adam')
cmd:option("-core", 2, 'cpu core') cmd:option("-crop_size", 46, 'crop size')
cmd:option("-max_size", 256, 'if image is larger than max_size, image will be crop to max_size randomly')
cmd:option("-batch_size", 8, 'mini batch size')
cmd:option("-patches", 16, 'number of patch samples')
cmd:option("-inner_epoch", 4, 'number of inner epochs')
cmd:option("-epoch", 30, 'number of epochs to run')
cmd:option("-thread", -1, 'number of CPU threads')
cmd:option("-jpeg_chroma_subsampling_rate", 0.0, 'the rate of YUV 4:2:0/YUV 4:4:4 in denoising training (0.0-1.0)')
cmd:option("-validation_rate", 0.05, 'validation-set rate (number_of_training_images * validation_rate > 1)')
cmd:option("-validation_crops", 80, 'number of cropping region per image in validation')
cmd:option("-active_cropping_rate", 0.5, 'active cropping rate')
cmd:option("-active_cropping_tries", 10, 'active cropping tries')
cmd:option("-nr_rate", 0.75, 'trade-off between reducing noise and erasing details (0.0-1.0)')
cmd:option("-save_history", 0, 'save all model (0|1)')
local opt = cmd:parse(arg) local opt = cmd:parse(arg)
for k, v in pairs(opt) do for k, v in pairs(opt) do
settings[k] = v settings[k] = v
end end
if settings.method == "noise" then if settings.save_history == 1 then
settings.model_file = string.format("%s/noise%d_model.t7", settings.save_history = true
settings.model_dir, settings.noise_level)
elseif settings.method == "scale" then
settings.model_file = string.format("%s/scale%.1fx_model.t7",
settings.model_dir, settings.scale)
elseif settings.method == "noise_scale" then
settings.model_file = string.format("%s/noise%d_scale%.1fx_model.t7",
settings.model_dir, settings.noise_level, settings.scale)
else else
error("unknown method: " .. settings.method) settings.save_history = false
end
if settings.save_history then
if settings.method == "noise" then
settings.model_file = string.format("%s/noise%d_model.%%d-%%d.t7",
settings.model_dir, settings.noise_level)
elseif settings.method == "scale" then
settings.model_file = string.format("%s/scale%.1fx_model.%%d-%%d.t7",
settings.model_dir, settings.scale)
else
error("unknown method: " .. settings.method)
end
else
if settings.method == "noise" then
settings.model_file = string.format("%s/noise%d_model.t7",
settings.model_dir, settings.noise_level)
elseif settings.method == "scale" then
settings.model_file = string.format("%s/scale%.1fx_model.t7",
settings.model_dir, settings.scale)
else
error("unknown method: " .. settings.method)
end
end end
if not (settings.color == "rgb" or settings.color == "y") then if not (settings.color == "rgb" or settings.color == "y") then
error("color must be y or rgb") error("color must be y or rgb")
@ -53,26 +84,16 @@ end
if not (settings.scale == math.floor(settings.scale) and settings.scale % 2 == 0) then if not (settings.scale == math.floor(settings.scale) and settings.scale % 2 == 0) then
error("scale must be mod-2") error("scale must be mod-2")
end end
if settings.random_half == 1 then if not (settings.style == "art" or
settings.random_half = true settings.style == "photo") then
else error(string.format("unknown style: %s", settings.style))
settings.random_half = false end
if settings.thread > 0 then
torch.setnumthreads(tonumber(settings.thread))
end end
torch.setnumthreads(settings.core)
settings.images = string.format("%s/images.t7", settings.data_dir) settings.images = string.format("%s/images.t7", settings.data_dir)
settings.image_list = string.format("%s/image_list.txt", settings.data_dir) settings.image_list = string.format("%s/image_list.txt", settings.data_dir)
settings.validation_ratio = 0.1
settings.validation_crops = 40
local srcnn = require './srcnn'
if (settings.method == "scale" or settings.method == "noise_scale") and settings.scale == 4 then
settings.create_model = srcnn.waifu4x
settings.block_offset = 13
else
settings.create_model = srcnn.waifu2x
settings.block_offset = 7
end
return settings return settings

View file

@ -1,74 +1,82 @@
require './LeakyReLU' require 'w2nn'
-- ref: http://arxiv.org/abs/1502.01852
-- ref: http://arxiv.org/abs/1501.00092
local srcnn = {}
function nn.SpatialConvolutionMM:reset(stdv) function nn.SpatialConvolutionMM:reset(stdv)
stdv = math.sqrt(2 / ( self.kW * self.kH * self.nOutputPlane)) stdv = math.sqrt(2 / ((1.0 + 0.1 * 0.1) * self.kW * self.kH * self.nOutputPlane))
self.weight:normal(0, stdv) self.weight:normal(0, stdv)
self.bias:fill(0) self.bias:zero()
end end
local srcnn = {} if cudnn and cudnn.SpatialConvolution then
function srcnn.waifu2x(color) function cudnn.SpatialConvolution:reset(stdv)
local model = nn.Sequential() stdv = math.sqrt(2 / ((1.0 + 0.1 * 0.1) * self.kW * self.kH * self.nOutputPlane))
local ch = nil self.weight:normal(0, stdv)
if color == "rgb" then self.bias:zero()
ch = 3
elseif color == "y" then
ch = 1
else
if color then
error("unknown color: " .. color)
else
error("unknown color: nil")
end
end end
end
function srcnn.channels(model)
return model:get(model:size() - 1).weight:size(1)
end
function srcnn.waifu2x_cunn(ch)
local model = nn.Sequential()
model:add(nn.SpatialConvolutionMM(ch, 32, 3, 3, 1, 1, 0, 0)) model:add(nn.SpatialConvolutionMM(ch, 32, 3, 3, 1, 1, 0, 0))
model:add(nn.LeakyReLU(0.1)) model:add(w2nn.LeakyReLU(0.1))
model:add(nn.SpatialConvolutionMM(32, 32, 3, 3, 1, 1, 0, 0)) model:add(nn.SpatialConvolutionMM(32, 32, 3, 3, 1, 1, 0, 0))
model:add(nn.LeakyReLU(0.1)) model:add(w2nn.LeakyReLU(0.1))
model:add(nn.SpatialConvolutionMM(32, 64, 3, 3, 1, 1, 0, 0)) model:add(nn.SpatialConvolutionMM(32, 64, 3, 3, 1, 1, 0, 0))
model:add(nn.LeakyReLU(0.1)) model:add(w2nn.LeakyReLU(0.1))
model:add(nn.SpatialConvolutionMM(64, 64, 3, 3, 1, 1, 0, 0)) model:add(nn.SpatialConvolutionMM(64, 64, 3, 3, 1, 1, 0, 0))
model:add(nn.LeakyReLU(0.1)) model:add(w2nn.LeakyReLU(0.1))
model:add(nn.SpatialConvolutionMM(64, 128, 3, 3, 1, 1, 0, 0)) model:add(nn.SpatialConvolutionMM(64, 128, 3, 3, 1, 1, 0, 0))
model:add(nn.LeakyReLU(0.1)) model:add(w2nn.LeakyReLU(0.1))
model:add(nn.SpatialConvolutionMM(128, 128, 3, 3, 1, 1, 0, 0)) model:add(nn.SpatialConvolutionMM(128, 128, 3, 3, 1, 1, 0, 0))
model:add(nn.LeakyReLU(0.1)) model:add(w2nn.LeakyReLU(0.1))
model:add(nn.SpatialConvolutionMM(128, ch, 3, 3, 1, 1, 0, 0)) model:add(nn.SpatialConvolutionMM(128, ch, 3, 3, 1, 1, 0, 0))
model:add(nn.View(-1):setNumInputDims(3)) model:add(nn.View(-1):setNumInputDims(3))
--model:cuda() --model:cuda()
--print(model:forward(torch.Tensor(32, 1, 92, 92):uniform():cuda()):size()) --print(model:forward(torch.Tensor(32, ch, 92, 92):uniform():cuda()):size())
return model, 7 return model
end end
function srcnn.waifu2x_cudnn(ch)
-- current 4x is worse than 2x * 2
function srcnn.waifu4x(color)
local model = nn.Sequential() local model = nn.Sequential()
model:add(cudnn.SpatialConvolution(ch, 32, 3, 3, 1, 1, 0, 0))
local ch = nil model:add(w2nn.LeakyReLU(0.1))
model:add(cudnn.SpatialConvolution(32, 32, 3, 3, 1, 1, 0, 0))
model:add(w2nn.LeakyReLU(0.1))
model:add(cudnn.SpatialConvolution(32, 64, 3, 3, 1, 1, 0, 0))
model:add(w2nn.LeakyReLU(0.1))
model:add(cudnn.SpatialConvolution(64, 64, 3, 3, 1, 1, 0, 0))
model:add(w2nn.LeakyReLU(0.1))
model:add(cudnn.SpatialConvolution(64, 128, 3, 3, 1, 1, 0, 0))
model:add(w2nn.LeakyReLU(0.1))
model:add(cudnn.SpatialConvolution(128, 128, 3, 3, 1, 1, 0, 0))
model:add(w2nn.LeakyReLU(0.1))
model:add(cudnn.SpatialConvolution(128, ch, 3, 3, 1, 1, 0, 0))
model:add(nn.View(-1):setNumInputDims(3))
--model:cuda()
--print(model:forward(torch.Tensor(32, ch, 92, 92):uniform():cuda()):size())
return model
end
function srcnn.create(model_name, backend, color)
local ch = 3
if color == "rgb" then if color == "rgb" then
ch = 3 ch = 3
elseif color == "y" then elseif color == "y" then
ch = 1 ch = 1
else else
error("unknown color: " .. color) error("unsupported color: " + color)
end
if backend == "cunn" then
return srcnn.waifu2x_cunn(ch)
elseif backend == "cudnn" then
return srcnn.waifu2x_cudnn(ch)
else
error("unsupported backend: " + backend)
end end
model:add(nn.SpatialConvolutionMM(ch, 32, 9, 9, 1, 1, 0, 0))
model:add(nn.LeakyReLU(0.1))
model:add(nn.SpatialConvolutionMM(32, 32, 3, 3, 1, 1, 0, 0))
model:add(nn.LeakyReLU(0.1))
model:add(nn.SpatialConvolutionMM(32, 64, 5, 5, 1, 1, 0, 0))
model:add(nn.LeakyReLU(0.1))
model:add(nn.SpatialConvolutionMM(64, 64, 3, 3, 1, 1, 0, 0))
model:add(nn.LeakyReLU(0.1))
model:add(nn.SpatialConvolutionMM(64, 128, 5, 5, 1, 1, 0, 0))
model:add(nn.LeakyReLU(0.1))
model:add(nn.SpatialConvolutionMM(128, 128, 3, 3, 1, 1, 0, 0))
model:add(nn.LeakyReLU(0.1))
model:add(nn.SpatialConvolutionMM(128, ch, 5, 5, 1, 1, 0, 0))
model:add(nn.View(-1):setNumInputDims(3))
return model, 13
end end
return srcnn return srcnn

26
lib/w2nn.lua Normal file
View file

@ -0,0 +1,26 @@
local function load_nn()
require 'torch'
require 'nn'
end
local function load_cunn()
require 'cutorch'
require 'cunn'
end
local function load_cudnn()
require 'cudnn'
cudnn.benchmark = true
end
if w2nn then
return w2nn
else
pcall(load_cunn)
pcall(load_cudnn)
w2nn = {}
require 'LeakyReLU'
require 'LeakyReLU_deprecated'
require 'DepthExpand2x'
require 'WeightedMSECriterion'
require 'ClippedWeightedHuberCriterion'
require 'cleanup_model'
return w2nn
end

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

209
tools/benchmark.lua Normal file
View file

@ -0,0 +1,209 @@
require 'pl'
local __FILE__ = (function() return string.gsub(debug.getinfo(2, 'S').source, "^@", "") end)()
package.path = path.join(path.dirname(__FILE__), "..", "lib", "?.lua;") .. package.path
require 'xlua'
require 'w2nn'
local iproc = require 'iproc'
local reconstruct = require 'reconstruct'
local image_loader = require 'image_loader'
local gm = require 'graphicsmagick'
local cmd = torch.CmdLine()
cmd:text()
cmd:text("waifu2x-benchmark")
cmd:text("Options:")
cmd:option("-dir", "./data/test", 'test image directory')
cmd:option("-model1_dir", "./models/anime_style_art_rgb", 'model1 directory')
cmd:option("-model2_dir", "", 'model2 directory (optional)')
cmd:option("-method", "scale", '(scale|noise)')
cmd:option("-filter", "Catrom", "downscaling filter (Box|Lanczos|Catrom(Bicubic))")
cmd:option("-color", "y", '(rgb|y)')
cmd:option("-noise_level", 1, 'model noise level')
cmd:option("-jpeg_quality", 75, 'jpeg quality')
cmd:option("-jpeg_times", 1, 'jpeg compression times')
cmd:option("-jpeg_quality_down", 5, 'value of jpeg quality to decrease each times')
cmd:option("-range_bug", 0, 'Reproducing the dynamic range bug that is caused by MATLAB\'s rgb2ycbcr(1|0)')
local opt = cmd:parse(arg)
torch.setdefaulttensortype('torch.FloatTensor')
if cudnn then
cudnn.fastest = true
cudnn.benchmark = false
end
local function rgb2y_matlab(x)
local y = torch.Tensor(1, x:size(2), x:size(3)):zero()
x = iproc.byte2float(x)
y:add(x[1] * 65.481)
y:add(x[2] * 128.553)
y:add(x[3] * 24.966)
y:add(16.0)
return y:byte():float()
end
local function RGBMSE(x1, x2)
x1 = iproc.float2byte(x1):float()
x2 = iproc.float2byte(x2):float()
return (x1 - x2):pow(2):mean()
end
local function YMSE(x1, x2)
if opt.range_bug == 1 then
local x1_2 = rgb2y_matlab(x1)
local x2_2 = rgb2y_matlab(x2)
return (x1_2 - x2_2):pow(2):mean()
else
local x1_2 = image.rgb2y(x1):mul(255.0)
local x2_2 = image.rgb2y(x2):mul(255.0)
return (x1_2 - x2_2):pow(2):mean()
end
end
local function MSE(x1, x2, color)
if color == "y" then
return YMSE(x1, x2)
else
return RGBMSE(x1, x2)
end
end
local function PSNR(x1, x2, color)
local mse = MSE(x1, x2, color)
return 10 * math.log10((255.0 * 255.0) / mse)
end
local function transform_jpeg(x, opt)
for i = 1, opt.jpeg_times do
jpeg = gm.Image(x, "RGB", "DHW")
jpeg:format("jpeg")
jpeg:samplingFactors({1.0, 1.0, 1.0})
blob, len = jpeg:toBlob(opt.jpeg_quality - (i - 1) * opt.jpeg_quality_down)
jpeg:fromBlob(blob, len)
x = jpeg:toTensor("byte", "RGB", "DHW")
end
return iproc.byte2float(x)
end
local function baseline_scale(x, filter)
return iproc.scale(x,
x:size(3) * 2.0,
x:size(2) * 2.0,
filter)
end
local function transform_scale(x, opt)
return iproc.scale(x,
x:size(3) * 0.5,
x:size(2) * 0.5,
opt.filter)
end
local function benchmark(opt, x, input_func, model1, model2)
local model1_mse = 0
local model2_mse = 0
local baseline_mse = 0
local model1_psnr = 0
local model2_psnr = 0
local baseline_psnr = 0
for i = 1, #x do
local ground_truth = x[i]
local input, model1_output, model2_output, baseline_output
input = input_func(ground_truth, opt)
t = sys.clock()
if input:size(3) == ground_truth:size(3) then
model1_output = reconstruct.image(model1, input)
if model2 then
model2_output = reconstruct.image(model2, input)
end
else
model1_output = reconstruct.scale(model1, 2.0, input)
if model2 then
model2_output = reconstruct.scale(model2, 2.0, input)
end
baseline_output = baseline_scale(input, opt.filter)
end
model1_mse = model1_mse + MSE(ground_truth, model1_output, opt.color)
model1_psnr = model1_psnr + PSNR(ground_truth, model1_output, opt.color)
if model2 then
model2_mse = model2_mse + MSE(ground_truth, model2_output, opt.color)
model2_psnr = model2_psnr + PSNR(ground_truth, model2_output, opt.color)
end
if baseline_output then
baseline_mse = baseline_mse + MSE(ground_truth, baseline_output, opt.color)
baseline_psnr = baseline_psnr + PSNR(ground_truth, baseline_output, opt.color)
end
if model2 then
if baseline_output then
io.stdout:write(
string.format("%d/%d; baseline_rmse=%f, model1_rmse=%f, model2_rmse=%f, baseline_psnr=%f, model1_psnr=%f, model2_psnr=%f \r",
i, #x,
math.sqrt(baseline_mse / i),
math.sqrt(model1_mse / i), math.sqrt(model2_mse / i),
baseline_psnr / i,
model1_psnr / i, model2_psnr / i
))
else
io.stdout:write(
string.format("%d/%d; model1_rmse=%f, model2_rmse=%f, model1_psnr=%f, model2_psnr=%f \r",
i, #x,
math.sqrt(model1_mse / i), math.sqrt(model2_mse / i),
model1_psnr / i, model2_psnr / i
))
end
else
if baseline_output then
io.stdout:write(
string.format("%d/%d; baseline_rmse=%f, model1_rmse=%f, baseline_psnr=%f, model1_psnr=%f \r",
i, #x,
math.sqrt(baseline_mse / i), math.sqrt(model1_mse / i),
baseline_psnr / i, model1_psnr / i
))
else
io.stdout:write(
string.format("%d/%d; model1_rmse=%f, model1_psnr=%f \r",
i, #x,
math.sqrt(model1_mse / i), model1_psnr / i
))
end
end
io.stdout:flush()
end
io.stdout:write("\n")
end
local function load_data(test_dir)
local test_x = {}
local files = dir.getfiles(test_dir, "*.*")
for i = 1, #files do
table.insert(test_x, iproc.crop_mod4(image_loader.load_float(files[i])))
xlua.progress(i, #files)
end
return test_x
end
function load_model(filename)
return torch.load(filename, "ascii")
end
print(opt)
if opt.method == "scale" then
local f1 = path.join(opt.model1_dir, "scale2.0x_model.t7")
local f2 = path.join(opt.model2_dir, "scale2.0x_model.t7")
local s1, model1 = pcall(load_model, f1)
local s2, model2 = pcall(load_model, f2)
if not s1 then
error("Load error: " .. f1)
end
if not s2 then
model2 = nil
end
local test_x = load_data(opt.dir)
benchmark(opt, test_x, transform_scale, model1, model2)
elseif opt.method == "noise" then
local f1 = path.join(opt.model1_dir, string.format("noise%d_model.t7", opt.noise_level))
local f2 = path.join(opt.model2_dir, string.format("noise%d_model.t7", opt.noise_level))
local s1, model1 = pcall(load_model, f1)
local s2, model2 = pcall(load_model, f2)
if not s1 then
error("Load error: " .. f1)
end
if not s2 then
model2 = nil
end
local test_x = load_data(opt.dir)
benchmark(opt, test_x, transform_jpeg, model1, model2)
end

25
tools/cleanup_model.lua Normal file
View file

@ -0,0 +1,25 @@
require 'pl'
local __FILE__ = (function() return string.gsub(debug.getinfo(2, 'S').source, "^@", "") end)()
package.path = path.join(path.dirname(__FILE__), "..", "lib", "?.lua;") .. package.path
require 'w2nn'
torch.setdefaulttensortype("torch.FloatTensor")
local cmd = torch.CmdLine()
cmd:text()
cmd:text("cleanup model")
cmd:text("Options:")
cmd:option("-model", "./model.t7", 'path of model file')
cmd:option("-iformat", "binary", 'input format')
cmd:option("-oformat", "binary", 'output format')
local opt = cmd:parse(arg)
local model = torch.load(opt.model, opt.iformat)
if model then
w2nn.cleanup_model(model)
model:cuda()
model:evaluate()
torch.save(opt.model, model, opt.oformat)
else
error("model not found")
end

43
tools/cudnn2cunn.lua Normal file
View file

@ -0,0 +1,43 @@
require 'pl'
local __FILE__ = (function() return string.gsub(debug.getinfo(2, 'S').source, "^@", "") end)()
package.path = path.join(path.dirname(__FILE__), "..", "lib", "?.lua;") .. package.path
require 'os'
require 'w2nn'
local srcnn = require 'srcnn'
local function cudnn2cunn(cudnn_model)
local cunn_model = srcnn.waifu2x_cunn(srcnn.channels(cudnn_model))
local weight_from = cudnn_model:findModules("cudnn.SpatialConvolution")
local weight_to = cunn_model:findModules("nn.SpatialConvolutionMM")
assert(#weight_from == #weight_to)
for i = 1, #weight_from do
local from = weight_from[i]
local to = weight_to[i]
to.weight:copy(from.weight)
to.bias:copy(from.bias)
end
cunn_model:cuda()
cunn_model:evaluate()
return cunn_model
end
local cmd = torch.CmdLine()
cmd:text()
cmd:text("waifu2x cudnn model to cunn model converter")
cmd:text("Options:")
cmd:option("-i", "", 'Specify the input cunn model')
cmd:option("-o", "", 'Specify the output cudnn model')
cmd:option("-iformat", "ascii", 'Specify the input format (ascii|binary)')
cmd:option("-oformat", "ascii", 'Specify the output format (ascii|binary)')
local opt = cmd:parse(arg)
if not path.isfile(opt.i) then
cmd:help()
os.exit(-1)
end
local cudnn_model = torch.load(opt.i, opt.iformat)
local cunn_model = cudnn2cunn(cudnn_model)
torch.save(opt.o, cunn_model, opt.oformat)

43
tools/cunn2cudnn.lua Normal file
View file

@ -0,0 +1,43 @@
require 'pl'
local __FILE__ = (function() return string.gsub(debug.getinfo(2, 'S').source, "^@", "") end)()
package.path = path.join(path.dirname(__FILE__), "..", "lib", "?.lua;") .. package.path
require 'os'
require 'w2nn'
local srcnn = require 'srcnn'
local function cunn2cudnn(cunn_model)
local cudnn_model = srcnn.waifu2x_cudnn(srcnn.channels(cunn_model))
local weight_from = cunn_model:findModules("nn.SpatialConvolutionMM")
local weight_to = cudnn_model:findModules("cudnn.SpatialConvolution")
assert(#weight_from == #weight_to)
for i = 1, #weight_from do
local from = weight_from[i]
local to = weight_to[i]
to.weight:copy(from.weight)
to.bias:copy(from.bias)
end
cudnn_model:cuda()
cudnn_model:evaluate()
return cudnn_model
end
local cmd = torch.CmdLine()
cmd:text()
cmd:text("waifu2x cunn model to cudnn model converter")
cmd:text("Options:")
cmd:option("-i", "", 'Specify the input cudnn model')
cmd:option("-o", "", 'Specify the output cunn model')
cmd:option("-iformat", "ascii", 'Specify the input format (ascii|binary)')
cmd:option("-oformat", "ascii", 'Specify the output format (ascii|binary)')
local opt = cmd:parse(arg)
if not path.isfile(opt.i) then
cmd:help()
os.exit(-1)
end
local cunn_model = torch.load(opt.i, opt.iformat)
local cudnn_model = cunn2cudnn(cunn_model)
torch.save(opt.o, cudnn_model, opt.oformat)

54
tools/export_model.lua Normal file
View file

@ -0,0 +1,54 @@
-- adapted from https://github.com/marcan/cl-waifu2x
require 'pl'
local __FILE__ = (function() return string.gsub(debug.getinfo(2, 'S').source, "^@", "") end)()
package.path = path.join(path.dirname(__FILE__), "..", "lib", "?.lua;") .. package.path
require 'w2nn'
local cjson = require "cjson"
function export(model, output)
local jmodules = {}
local modules = model:findModules("nn.SpatialConvolutionMM")
if #modules == 0 then
-- cudnn model
modules = model:findModules("cudnn.SpatialConvolution")
end
for i = 1, #modules, 1 do
local module = modules[i]
local jmod = {
kW = module.kW,
kH = module.kH,
nInputPlane = module.nInputPlane,
nOutputPlane = module.nOutputPlane,
bias = torch.totable(module.bias:float()),
weight = torch.totable(module.weight:float():reshape(module.nOutputPlane, module.nInputPlane, module.kW, module.kH))
}
table.insert(jmodules, jmod)
end
jmodules[1].color = "RGB"
jmodules[1].gamma = 0
jmodules[#jmodules].color = "RGB"
jmodules[#jmodules].gamma = 0
local fp = io.open(output, "w")
if not fp then
error("IO Error: " .. output)
end
fp:write(cjson.encode(jmodules))
fp:close()
end
local cmd = torch.CmdLine()
cmd:text()
cmd:text("waifu2x export model")
cmd:text("Options:")
cmd:option("-i", "input.t7", 'Specify the input torch model')
cmd:option("-o", "output.json", 'Specify the output json file')
cmd:option("-iformat", "ascii", 'Specify the input format (ascii|binary)')
local opt = cmd:parse(arg)
if not path.isfile(opt.i) then
cmd:help()
os.exit(-1)
end
local model = torch.load(opt.i, opt.iformat)
export(model, opt.o)

43
tools/rebuild_model.lua Normal file
View file

@ -0,0 +1,43 @@
require 'pl'
local __FILE__ = (function() return string.gsub(debug.getinfo(2, 'S').source, "^@", "") end)()
package.path = path.join(path.dirname(__FILE__), "..", "lib", "?.lua;") .. package.path
require 'os'
require 'w2nn'
local srcnn = require 'srcnn'
local function rebuild(old_model)
local new_model = srcnn.waifu2x_cunn(srcnn.channels(old_model))
local weight_from = old_model:findModules("nn.SpatialConvolutionMM")
local weight_to = new_model:findModules("nn.SpatialConvolutionMM")
assert(#weight_from == #weight_to)
for i = 1, #weight_from do
local from = weight_from[i]
local to = weight_to[i]
to.weight:copy(from.weight)
to.bias:copy(from.bias)
end
new_model:cuda()
new_model:evaluate()
return new_model
end
local cmd = torch.CmdLine()
cmd:text()
cmd:text("waifu2x rebuild cunn model")
cmd:text("Options:")
cmd:option("-i", "", 'Specify the input model')
cmd:option("-o", "", 'Specify the output model')
cmd:option("-iformat", "ascii", 'Specify the input format (ascii|binary)')
cmd:option("-oformat", "ascii", 'Specify the output format (ascii|binary)')
local opt = cmd:parse(arg)
if not path.isfile(opt.i) then
cmd:help()
os.exit(-1)
end
local old_model = torch.load(opt.i, opt.iformat)
local new_model = rebuild(old_model)
torch.save(opt.o, new_model, opt.oformat)

269
train.lua
View file

@ -1,21 +1,25 @@
require './lib/portable' require 'pl'
local __FILE__ = (function() return string.gsub(debug.getinfo(2, 'S').source, "^@", "") end)()
package.path = path.join(path.dirname(__FILE__), "lib", "?.lua;") .. package.path
require 'optim' require 'optim'
require 'xlua' require 'xlua'
require 'pl'
local settings = require './lib/settings' require 'w2nn'
local minibatch_adam = require './lib/minibatch_adam' local settings = require 'settings'
local iproc = require './lib/iproc' local srcnn = require 'srcnn'
local reconstruct = require './lib/reconstruct' local minibatch_adam = require 'minibatch_adam'
local pairwise_transform = require './lib/pairwise_transform' local iproc = require 'iproc'
local image_loader = require './lib/image_loader' local reconstruct = require 'reconstruct'
local compression = require 'compression'
local pairwise_transform = require 'pairwise_transform'
local image_loader = require 'image_loader'
local function save_test_scale(model, rgb, file) local function save_test_scale(model, rgb, file)
local up = reconstruct.scale(model, settings.scale, rgb, settings.block_offset) local up = reconstruct.scale(model, settings.scale, rgb)
image.save(file, up) image.save(file, up)
end end
local function save_test_jpeg(model, rgb, file) local function save_test_jpeg(model, rgb, file)
local im, count = reconstruct.image(model, rgb, settings.block_offset) local im, count = reconstruct.image(model, rgb)
image.save(file, im) image.save(file, im)
end end
local function split_data(x, test_size) local function split_data(x, test_size)
@ -31,14 +35,19 @@ local function split_data(x, test_size)
end end
return train_x, valid_x return train_x, valid_x
end end
local function make_validation_set(x, transformer, n) local function make_validation_set(x, transformer, n, patches)
n = n or 4 n = n or 4
local data = {} local data = {}
for i = 1, #x do for i = 1, #x do
for k = 1, n do for k = 1, math.max(n / patches, 1) do
local x, y = transformer(x[i], true) local xy = transformer(x[i], true, patches)
table.insert(data, {x = x:reshape(1, x:size(1), x:size(2), x:size(3)), local tx = torch.Tensor(patches, xy[1][1]:size(1), xy[1][1]:size(2), xy[1][1]:size(3))
y = y:reshape(1, y:size(1), y:size(2), y:size(3))}) local ty = torch.Tensor(patches, xy[1][2]:size(1), xy[1][2]:size(2), xy[1][2]:size(3))
for j = 1, #xy do
tx[j]:copy(xy[j][1])
ty[j]:copy(xy[j][2])
end
table.insert(data, {x = tx, y = ty})
end end
xlua.progress(i, #x) xlua.progress(i, #x)
collectgarbage() collectgarbage()
@ -50,113 +59,195 @@ local function validate(model, criterion, data)
for i = 1, #data do for i = 1, #data do
local z = model:forward(data[i].x:cuda()) local z = model:forward(data[i].x:cuda())
loss = loss + criterion:forward(z, data[i].y:cuda()) loss = loss + criterion:forward(z, data[i].y:cuda())
xlua.progress(i, #data) if i % 100 == 0 then
if i % 10 == 0 then xlua.progress(i, #data)
collectgarbage() collectgarbage()
end end
end end
xlua.progress(#data, #data)
return loss / #data return loss / #data
end end
local function create_criterion(model)
if reconstruct.is_rgb(model) then
local offset = reconstruct.offset_size(model)
local output_w = settings.crop_size - offset * 2
local weight = torch.Tensor(3, output_w * output_w)
weight[1]:fill(0.29891 * 3) -- R
weight[2]:fill(0.58661 * 3) -- G
weight[3]:fill(0.11448 * 3) -- B
return w2nn.ClippedWeightedHuberCriterion(weight, 0.1, {0.0, 1.0}):cuda()
else
return nn.MSECriterion():cuda()
end
end
local function transformer(x, is_validation, n, offset)
x = compression.decompress(x)
n = n or settings.patches
if is_validation == nil then is_validation = false end
local random_color_noise_rate = nil
local random_overlay_rate = nil
local active_cropping_rate = nil
local active_cropping_tries = nil
if is_validation then
active_cropping_rate = 0
active_cropping_tries = 0
random_color_noise_rate = 0.0
random_overlay_rate = 0.0
else
active_cropping_rate = settings.active_cropping_rate
active_cropping_tries = settings.active_cropping_tries
random_color_noise_rate = settings.random_color_noise_rate
random_overlay_rate = settings.random_overlay_rate
end
if settings.method == "scale" then
return pairwise_transform.scale(x,
settings.scale,
settings.crop_size, offset,
n,
{
random_half_rate = settings.random_half_rate,
random_color_noise_rate = random_color_noise_rate,
random_overlay_rate = random_overlay_rate,
random_unsharp_mask_rate = settings.random_unsharp_mask_rate,
max_size = settings.max_size,
active_cropping_rate = active_cropping_rate,
active_cropping_tries = active_cropping_tries,
rgb = (settings.color == "rgb")
})
elseif settings.method == "noise" then
return pairwise_transform.jpeg(x,
settings.style,
settings.noise_level,
settings.crop_size, offset,
n,
{
random_half_rate = settings.random_half_rate,
random_color_noise_rate = random_color_noise_rate,
random_overlay_rate = random_overlay_rate,
random_unsharp_mask_rate = settings.random_unsharp_mask_rate,
max_size = settings.max_size,
jpeg_chroma_subsampling_rate = settings.jpeg_chroma_subsampling_rate,
active_cropping_rate = active_cropping_rate,
active_cropping_tries = active_cropping_tries,
nr_rate = settings.nr_rate,
rgb = (settings.color == "rgb")
})
end
end
local function resampling(x, y, train_x, transformer, input_size, target_size)
print("## resampling")
for t = 1, #train_x do
xlua.progress(t, #train_x)
local xy = transformer(train_x[t], false, settings.patches)
for i = 1, #xy do
local index = (t - 1) * settings.patches + i
x[index]:copy(xy[i][1])
y[index]:copy(xy[i][2])
end
if t % 50 == 0 then
collectgarbage()
end
end
end
local function train() local function train()
local model, offset = settings.create_model(settings.color) local LR_MIN = 1.0e-5
assert(offset == settings.block_offset) local model = srcnn.create(settings.method, settings.backend, settings.color)
local criterion = nn.MSECriterion():cuda() local offset = reconstruct.offset_size(model)
local pairwise_func = function(x, is_validation, n)
return transformer(x, is_validation, n, offset)
end
local criterion = create_criterion(model)
local x = torch.load(settings.images) local x = torch.load(settings.images)
local lrd_count = 0 local train_x, valid_x = split_data(x, math.floor(settings.validation_rate * #x))
local train_x, valid_x = split_data(x,
math.floor(settings.validation_ratio * #x),
settings.validation_crops)
local test = image_loader.load_float(settings.test)
local adam_config = { local adam_config = {
learningRate = settings.learning_rate, learningRate = settings.learning_rate,
xBatchSize = settings.batch_size, xBatchSize = settings.batch_size,
} }
local lrd_count = 0
local ch = nil local ch = nil
if settings.color == "y" then if settings.color == "y" then
ch = 1 ch = 1
elseif settings.color == "rgb" then elseif settings.color == "rgb" then
ch = 3 ch = 3
end end
local transformer = function(x, is_validation)
if is_validation == nil then is_validation = false end
if settings.method == "scale" then
return pairwise_transform.scale(x,
settings.scale,
settings.crop_size, offset,
{ color_augment = not is_validation,
random_half = settings.random_half,
rgb = (settings.color == "rgb")
})
elseif settings.method == "noise" then
return pairwise_transform.jpeg(x,
settings.noise_level,
settings.crop_size, offset,
{ color_augment = not is_validation,
random_half = settings.random_half,
rgb = (settings.color == "rgb")
})
elseif settings.method == "noise_scale" then
return pairwise_transform.jpeg_scale(x,
settings.scale,
settings.noise_level,
settings.crop_size, offset,
{ color_augment = not is_validation,
random_half = settings.random_half,
rgb = (settings.color == "rgb")
})
end
end
local best_score = 100000.0 local best_score = 100000.0
print("# make validation-set") print("# make validation-set")
local valid_xy = make_validation_set(valid_x, transformer, 20) local valid_xy = make_validation_set(valid_x, pairwise_func,
settings.validation_crops,
settings.patches)
valid_x = nil valid_x = nil
collectgarbage() collectgarbage()
model:cuda() model:cuda()
print("load .. " .. #train_x) print("load .. " .. #train_x)
local x = torch.Tensor(settings.patches * #train_x,
ch, settings.crop_size, settings.crop_size)
local y = torch.Tensor(settings.patches * #train_x,
ch * (settings.crop_size - offset * 2) * (settings.crop_size - offset * 2)):zero()
for epoch = 1, settings.epoch do for epoch = 1, settings.epoch do
model:training() model:training()
print("# " .. epoch) print("# " .. epoch)
print(minibatch_adam(model, criterion, train_x, adam_config, resampling(x, y, train_x, pairwise_func)
transformer, for i = 1, settings.inner_epoch do
{ch, settings.crop_size, settings.crop_size}, print(minibatch_adam(model, criterion, x, y, adam_config))
{ch, settings.crop_size - offset * 2, settings.crop_size - offset * 2} model:evaluate()
)) print("# validation")
model:evaluate() local score = validate(model, criterion, valid_xy)
print("# validation") if score < best_score then
local score = validate(model, criterion, valid_xy) local test_image = image_loader.load_float(settings.test) -- reload
if score < best_score then
lrd_count = 0
best_score = score
print("* update best model")
torch.save(settings.model_file, model)
if settings.method == "noise" then
local log = path.join(settings.model_dir,
("noise%d_best.png"):format(settings.noise_level))
save_test_jpeg(model, test, log)
elseif settings.method == "scale" then
local log = path.join(settings.model_dir,
("scale%.1f_best.png"):format(settings.scale))
save_test_scale(model, test, log)
elseif settings.method == "noise_scale" then
local log = path.join(settings.model_dir,
("noise%d_scale%.1f_best.png"):format(settings.noise_level,
settings.scale))
save_test_scale(model, test, log)
end
else
lrd_count = lrd_count + 1
if lrd_count > 5 then
lrd_count = 0 lrd_count = 0
adam_config.learningRate = adam_config.learningRate * 0.8 best_score = score
print("* learning rate decay: " .. adam_config.learningRate) print("* update best model")
if settings.save_history then
local model_clone = model:clone()
w2nn.cleanup_model(model_clone)
torch.save(string.format(settings.model_file, epoch, i), model_clone)
if settings.method == "noise" then
local log = path.join(settings.model_dir,
("noise%d_best.%d-%d.png"):format(settings.noise_level,
epoch, i))
save_test_jpeg(model, test_image, log)
elseif settings.method == "scale" then
local log = path.join(settings.model_dir,
("scale%.1f_best.%d-%d.png"):format(settings.scale,
epoch, i))
save_test_scale(model, test_image, log)
end
else
torch.save(settings.model_file, model)
if settings.method == "noise" then
local log = path.join(settings.model_dir,
("noise%d_best.png"):format(settings.noise_level))
save_test_jpeg(model, test_image, log)
elseif settings.method == "scale" then
local log = path.join(settings.model_dir,
("scale%.1f_best.png"):format(settings.scale))
save_test_scale(model, test_image, log)
end
end
else
lrd_count = lrd_count + 1
if lrd_count > 2 and adam_config.learningRate > LR_MIN then
adam_config.learningRate = adam_config.learningRate * 0.8
print("* learning rate decay: " .. adam_config.learningRate)
lrd_count = 0
end
end end
print("current: " .. score .. ", best: " .. best_score)
collectgarbage()
end end
print("current: " .. score .. ", best: " .. best_score)
collectgarbage()
end end
end end
if settings.gpu > 0 then
cutorch.setDevice(settings.gpu)
end
torch.manualSeed(settings.seed) torch.manualSeed(settings.seed)
cutorch.manualSeed(settings.seed) cutorch.manualSeed(settings.seed)
print(settings) print(settings)

View file

@ -1,10 +1,12 @@
#!/bin/sh #!/bin/sh
th train.lua -color rgb -method noise -noise_level 1 -model_dir models/anime_style_art_rgb -test images/miku_noisy.png th convert_data.lua
th cleanup_model.lua -model models/anime_style_art_rgb/noise1_model.t7 -oformat ascii
th train.lua -color rgb -method noise -noise_level 2 -model_dir models/anime_style_art_rgb -test images/miku_noisy.png th train.lua -method scale -model_dir models/anime_style_art_rgb -test images/miku_small.png -thread 4
th cleanup_model.lua -model models/anime_style_art_rgb/noise2_model.t7 -oformat ascii th tools/cleanup_model.lua -model models/anime_style_art_rgb/scale2.0x_model.t7 -oformat ascii
th train.lua -color rgb -method scale -scale 2 -model_dir models/anime_style_art_rgb -test images/miku_small.png th train.lua -method noise -noise_level 1 -style art -model_dir models/anime_style_art_rgb -test images/miku_noisy.png -thread 4
th cleanup_model.lua -model models/anime_style_art_rgb/scale2.0x_model.t7 -oformat ascii th tools/cleanup_model.lua -model models/anime_style_art_rgb/noise1_model.t7 -oformat ascii
th train.lua -method noise -noise_level 2 -style art -model_dir models/anime_style_art_rgb -test images/miku_noisy.png -thread 4
th tools/cleanup_model.lua -model models/anime_style_art_rgb/noise2_model.t7 -oformat ascii

12
train_photo.sh Executable file
View file

@ -0,0 +1,12 @@
#!/bin/sh
th convert_data.lua -style photo -data_dir ./data/photo -model_dir models/photo
th train.lua -style photo -method scale -data_dir ./data/photo -model_dir models/photo_uk -test work/scale_test_photo.png -color rgb -thread 4 -backend cudnn -random_unsharp_mask_rate 0.1 -validation_crops 160
th tools/cleanup_model.lua -model models/photo/scale2.0x_model.t7 -oformat ascii
th train.lua -style photo -method noise -noise_level 1 -data_dir ./data/photo -model_dir models/photo -test work/noise_test_photo.jpg -color rgb -thread 4 -backend cudnn -random_unsharp_mask_rate 0.5 -validation_crops 160 -nr_rate 0.6 -epoch 33
th tools/cleanup_model.lua -model models/photo/noise1_model.t7 -oformat ascii
th train.lua -style photo -method noise -noise_level 2 -data_dir ./data/photo -model_dir models/photo -test work/noise_test_photo.jpg -color rgb -thread 4 -backend cudnn -random_unsharp_mask_rate 0.5 -validation_crops 160 -nr_rate 0.8 -epoch 38
th tools/cleanup_model.lua -model models/photo/noise2_model.t7 -oformat ascii

9
train_ukbench.sh Executable file
View file

@ -0,0 +1,9 @@
#!/bin/sh
th convert_data.lua -data_dir ./data/ukbench
#th train.lua -style photo -method noise -noise_level 2 -data_dir ./data/ukbench -model_dir models/ukbench -test images/lena.png -nr_rate 0.9 -jpeg_sampling_factors 420 # -thread 4 -backend cudnn
#th tools/cleanup_model.lua -model models/ukbench/noise2_model.t7 -oformat ascii
th train.lua -method scale -data_dir ./data/ukbench -model_dir models/ukbench -test images/lena.jpg # -thread 4 -backend cudnn
th tools/cleanup_model.lua -model models/ukbench/scale2.0x_model.t7 -oformat ascii

View file

@ -1,12 +1,12 @@
require './lib/portable'
require 'sys'
require 'pl' require 'pl'
require './lib/LeakyReLU' local __FILE__ = (function() return string.gsub(debug.getinfo(2, 'S').source, "^@", "") end)()
package.path = path.join(path.dirname(__FILE__), "lib", "?.lua;") .. package.path
local iproc = require './lib/iproc' require 'sys'
local reconstruct = require './lib/reconstruct' require 'w2nn'
local image_loader = require './lib/image_loader' local iproc = require 'iproc'
local BLOCK_OFFSET = 7 local reconstruct = require 'reconstruct'
local image_loader = require 'image_loader'
local alpha_util = require 'alpha_util'
torch.setdefaulttensortype('torch.FloatTensor') torch.setdefaulttensortype('torch.FloatTensor')
@ -14,43 +14,112 @@ local function convert_image(opt)
local x, alpha = image_loader.load_float(opt.i) local x, alpha = image_loader.load_float(opt.i)
local new_x = nil local new_x = nil
local t = sys.clock() local t = sys.clock()
local scale_f, image_f
if opt.tta == 1 then
scale_f = reconstruct.scale_tta
image_f = reconstruct.image_tta
else
scale_f = reconstruct.scale
image_f = reconstruct.image
end
if opt.o == "(auto)" then if opt.o == "(auto)" then
local name = path.basename(opt.i) local name = path.basename(opt.i)
local e = path.extension(name) local e = path.extension(name)
local base = name:sub(0, name:len() - e:len()) local base = name:sub(0, name:len() - e:len())
opt.o = path.join(path.dirname(opt.i), string.format("%s(%s).png", base, opt.m)) opt.o = path.join(path.dirname(opt.i), string.format("%s_%s.png", base, opt.m))
end end
if opt.m == "noise" then if opt.m == "noise" then
local model = torch.load(path.join(opt.model_dir, ("noise%d_model.t7"):format(opt.noise_level)), "ascii") local model_path = path.join(opt.model_dir, ("noise%d_model.t7"):format(opt.noise_level))
model:evaluate() local model = torch.load(model_path, "ascii")
new_x = reconstruct.image(model, x, BLOCK_OFFSET, opt.crop_size) if not model then
error("Load Error: " .. model_path)
end
new_x = image_f(model, x, opt.crop_size)
new_x = alpha_util.composite(new_x, alpha)
elseif opt.m == "scale" then elseif opt.m == "scale" then
local model = torch.load(path.join(opt.model_dir, ("scale%.1fx_model.t7"):format(opt.scale)), "ascii") local model_path = path.join(opt.model_dir, ("scale%.1fx_model.t7"):format(opt.scale))
model:evaluate() local model = torch.load(model_path, "ascii")
new_x = reconstruct.scale(model, opt.scale, x, BLOCK_OFFSET, opt.crop_size) if not model then
error("Load Error: " .. model_path)
end
x = alpha_util.make_border(x, alpha, reconstruct.offset_size(model))
new_x = scale_f(model, opt.scale, x, opt.crop_size)
new_x = alpha_util.composite(new_x, alpha, model)
elseif opt.m == "noise_scale" then elseif opt.m == "noise_scale" then
local noise_model = torch.load(path.join(opt.model_dir, ("noise%d_model.t7"):format(opt.noise_level)), "ascii") local noise_model_path = path.join(opt.model_dir, ("noise%d_model.t7"):format(opt.noise_level))
local scale_model = torch.load(path.join(opt.model_dir, ("scale%.1fx_model.t7"):format(opt.scale)), "ascii") local noise_model = torch.load(noise_model_path, "ascii")
noise_model:evaluate() local scale_model_path = path.join(opt.model_dir, ("scale%.1fx_model.t7"):format(opt.scale))
scale_model:evaluate() local scale_model = torch.load(scale_model_path, "ascii")
x = reconstruct.image(noise_model, x, BLOCK_OFFSET)
new_x = reconstruct.scale(scale_model, opt.scale, x, BLOCK_OFFSET, opt.crop_size) if not noise_model then
error("Load Error: " .. noise_model_path)
end
if not scale_model then
error("Load Error: " .. scale_model_path)
end
x = alpha_util.make_border(x, alpha, reconstruct.offset_size(scale_model))
x = image_f(noise_model, x, opt.crop_size)
new_x = scale_f(scale_model, opt.scale, x, opt.crop_size)
new_x = alpha_util.composite(new_x, alpha, scale_model)
else else
error("undefined method:" .. opt.method) error("undefined method:" .. opt.method)
end end
image_loader.save_png(opt.o, new_x, alpha) image_loader.save_png(opt.o, new_x, opt.depth)
print(opt.o .. ": " .. (sys.clock() - t) .. " sec") print(opt.o .. ": " .. (sys.clock() - t) .. " sec")
end end
local function convert_frames(opt) local function convert_frames(opt)
local noise1_model = torch.load(path.join(opt.model_dir, "noise1_model.t7"), "ascii") local model_path, noise1_model, noise2_model, scale_model
local noise2_model = torch.load(path.join(opt.model_dir, "noise2_model.t7"), "ascii") local scale_f, image_f
local scale_model = torch.load(path.join(opt.model_dir, ("scale%.1fx_model.t7"):format(opt.scale)), "ascii") if opt.tta == 1 then
scale_f = reconstruct.scale_tta
noise1_model:evaluate() image_f = reconstruct.image_tta
noise2_model:evaluate() else
scale_model:evaluate() scale_f = reconstruct.scale
image_f = reconstruct.image
end
if opt.m == "scale" then
model_path = path.join(opt.model_dir, ("scale%.1fx_model.t7"):format(opt.scale))
scale_model = torch.load(model_path, "ascii")
if not scale_model then
error("Load Error: " .. model_path)
end
elseif opt.m == "noise" and opt.noise_level == 1 then
model_path = path.join(opt.model_dir, "noise1_model.t7")
noise1_model = torch.load(model_path, "ascii")
if not noise1_model then
error("Load Error: " .. model_path)
end
elseif opt.m == "noise" and opt.noise_level == 2 then
model_path = path.join(opt.model_dir, "noise2_model.t7")
noise2_model = torch.load(model_path, "ascii")
if not noise2_model then
error("Load Error: " .. model_path)
end
elseif opt.m == "noise_scale" then
model_path = path.join(opt.model_dir, ("scale%.1fx_model.t7"):format(opt.scale))
scale_model = torch.load(model_path, "ascii")
if not scale_model then
error("Load Error: " .. model_path)
end
if opt.noise_level == 1 then
model_path = path.join(opt.model_dir, "noise1_model.t7")
noise1_model = torch.load(model_path, "ascii")
if not noise1_model then
error("Load Error: " .. model_path)
end
elseif opt.noise_level == 2 then
model_path = path.join(opt.model_dir, "noise2_model.t7")
noise2_model = torch.load(model_path, "ascii")
if not noise2_model then
error("Load Error: " .. model_path)
end
end
end
local fp = io.open(opt.l) local fp = io.open(opt.l)
if not fp then
error("Open Error: " .. opt.l)
end
local count = 0 local count = 0
local lines = {} local lines = {}
for line in fp:lines() do for line in fp:lines() do
@ -62,17 +131,25 @@ local function convert_frames(opt)
local x, alpha = image_loader.load_float(lines[i]) local x, alpha = image_loader.load_float(lines[i])
local new_x = nil local new_x = nil
if opt.m == "noise" and opt.noise_level == 1 then if opt.m == "noise" and opt.noise_level == 1 then
new_x = reconstruct.image(noise1_model, x, BLOCK_OFFSET, opt.crop_size) new_x = image_f(noise1_model, x, opt.crop_size)
new_x = alpha_util.composite(new_x, alpha)
elseif opt.m == "noise" and opt.noise_level == 2 then elseif opt.m == "noise" and opt.noise_level == 2 then
new_x = reconstruct.image(noise2_model, x, BLOCK_OFFSET) new_x = image_f(noise2_model, x, opt.crop_size)
new_x = alpha_util.composite(new_x, alpha)
elseif opt.m == "scale" then elseif opt.m == "scale" then
new_x = reconstruct.scale(scale_model, opt.scale, x, BLOCK_OFFSET, opt.crop_size) x = alpha_util.make_border(x, alpha, reconstruct.offset_size(scale_model))
new_x = scale_f(scale_model, opt.scale, x, opt.crop_size)
new_x = alpha_util.composite(new_x, alpha, scale_model)
elseif opt.m == "noise_scale" and opt.noise_level == 1 then elseif opt.m == "noise_scale" and opt.noise_level == 1 then
x = reconstruct.image(noise1_model, x, BLOCK_OFFSET) x = alpha_util.make_border(x, alpha, reconstruct.offset_size(scale_model))
new_x = reconstruct.scale(scale_model, opt.scale, x, BLOCK_OFFSET, opt.crop_size) x = image_f(noise1_model, x, opt.crop_size)
new_x = scale_f(scale_model, opt.scale, x, opt.crop_size)
new_x = alpha_util.composite(new_x, alpha, scale_model)
elseif opt.m == "noise_scale" and opt.noise_level == 2 then elseif opt.m == "noise_scale" and opt.noise_level == 2 then
x = reconstruct.image(noise2_model, x, BLOCK_OFFSET) x = alpha_util.make_border(x, alpha, reconstruct.offset_size(scale_model))
new_x = reconstruct.scale(scale_model, opt.scale, x, BLOCK_OFFSET, opt.crop_size) x = image_f(noise2_model, x, opt.crop_size)
new_x = scale_f(scale_model, opt.scale, x, opt.crop_size)
new_x = alpha_util.composite(new_x, alpha, scale_model)
else else
error("undefined method:" .. opt.method) error("undefined method:" .. opt.method)
end end
@ -85,7 +162,7 @@ local function convert_frames(opt)
else else
output = string.format(opt.o, i) output = string.format(opt.o, i)
end end
image_loader.save_png(output, new_x, alpha) image_loader.save_png(output, new_x, opt.depth)
xlua.progress(i, #lines) xlua.progress(i, #lines)
if i % 10 == 0 then if i % 10 == 0 then
collectgarbage() collectgarbage()
@ -101,17 +178,28 @@ local function waifu2x()
cmd:text() cmd:text()
cmd:text("waifu2x") cmd:text("waifu2x")
cmd:text("Options:") cmd:text("Options:")
cmd:option("-i", "images/miku_small.png", 'path of the input image') cmd:option("-i", "images/miku_small.png", 'path to input image')
cmd:option("-l", "", 'path of the image-list') cmd:option("-l", "", 'path to image-list.txt')
cmd:option("-scale", 2, 'scale factor') cmd:option("-scale", 2, 'scale factor')
cmd:option("-o", "(auto)", 'path of the output file') cmd:option("-o", "(auto)", 'path to output file')
cmd:option("-model_dir", "./models/anime_style_art_rgb", 'model directory') cmd:option("-depth", 8, 'bit-depth of the output image (8|16)')
cmd:option("-model_dir", "./models/anime_style_art_rgb", 'path to model directory')
cmd:option("-m", "noise_scale", 'method (noise|scale|noise_scale)') cmd:option("-m", "noise_scale", 'method (noise|scale|noise_scale)')
cmd:option("-noise_level", 1, '(1|2)') cmd:option("-noise_level", 1, '(1|2)')
cmd:option("-crop_size", 128, 'patch size per process') cmd:option("-crop_size", 128, 'patch size per process')
cmd:option("-resume", 0, "skip existing files (0|1)") cmd:option("-resume", 0, "skip existing files (0|1)")
cmd:option("-thread", -1, "number of CPU threads")
cmd:option("-tta", 0, '8x slower and slightly high quality (0|1)')
local opt = cmd:parse(arg) local opt = cmd:parse(arg)
if opt.thread > 0 then
torch.setnumthreads(opt.thread)
end
if cudnn then
cudnn.fastest = true
cudnn.benchmark = false
end
if string.len(opt.l) == 0 then if string.len(opt.l) == 0 then
convert_image(opt) convert_image(opt)
else else

267
web.lua
View file

@ -1,11 +1,23 @@
require 'pl'
local __FILE__ = (function() return string.gsub(debug.getinfo(2, 'S').source, "^@", "") end)()
local ROOT = path.dirname(__FILE__)
package.path = path.join(ROOT, "lib", "?.lua;") .. package.path
_G.TURBO_SSL = true _G.TURBO_SSL = true
local turbo = require 'turbo'
require 'w2nn'
local uuid = require 'uuid' local uuid = require 'uuid'
local ffi = require 'ffi' local ffi = require 'ffi'
local md5 = require 'md5' local md5 = require 'md5'
require 'pl' local iproc = require 'iproc'
require './lib/portable' local reconstruct = require 'reconstruct'
require './lib/LeakyReLU' local image_loader = require 'image_loader'
local alpha_util = require 'alpha_util'
local gm = require 'graphicsmagick'
-- Note: turbo and xlua has different implementation of string:split().
-- Therefore, string:split() has conflict issue.
-- In this script, use turbo's string:split().
local turbo = require 'turbo'
local cmd = torch.CmdLine() local cmd = torch.CmdLine()
cmd:text() cmd:text()
@ -13,34 +25,36 @@ cmd:text("waifu2x-api")
cmd:text("Options:") cmd:text("Options:")
cmd:option("-port", 8812, 'listen port') cmd:option("-port", 8812, 'listen port')
cmd:option("-gpu", 1, 'Device ID') cmd:option("-gpu", 1, 'Device ID')
cmd:option("-core", 2, 'number of CPU cores') cmd:option("-thread", -1, 'number of CPU threads')
local opt = cmd:parse(arg) local opt = cmd:parse(arg)
cutorch.setDevice(opt.gpu) cutorch.setDevice(opt.gpu)
torch.setdefaulttensortype('torch.FloatTensor') torch.setdefaulttensortype('torch.FloatTensor')
torch.setnumthreads(opt.core) if opt.thread > 0 then
torch.setnumthreads(opt.thread)
local iproc = require './lib/iproc' end
local reconstruct = require './lib/reconstruct' if cudnn then
local image_loader = require './lib/image_loader' cudnn.fastest = true
cudnn.benchmark = false
local MODEL_DIR = "./models/anime_style_art_rgb" end
local ART_MODEL_DIR = path.join(ROOT, "models", "anime_style_art_rgb")
local noise1_model = torch.load(path.join(MODEL_DIR, "noise1_model.t7"), "ascii") local PHOTO_MODEL_DIR = path.join(ROOT, "models", "photo")
local noise2_model = torch.load(path.join(MODEL_DIR, "noise2_model.t7"), "ascii") local art_noise1_model = torch.load(path.join(ART_MODEL_DIR, "noise1_model.t7"), "ascii")
local scale20_model = torch.load(path.join(MODEL_DIR, "scale2.0x_model.t7"), "ascii") local art_noise2_model = torch.load(path.join(ART_MODEL_DIR, "noise2_model.t7"), "ascii")
local art_scale2_model = torch.load(path.join(ART_MODEL_DIR, "scale2.0x_model.t7"), "ascii")
local USE_CACHE = true local photo_scale2_model = torch.load(path.join(PHOTO_MODEL_DIR, "scale2.0x_model.t7"), "ascii")
local CACHE_DIR = "./cache" local photo_noise1_model = torch.load(path.join(PHOTO_MODEL_DIR, "noise1_model.t7"), "ascii")
local photo_noise2_model = torch.load(path.join(PHOTO_MODEL_DIR, "noise2_model.t7"), "ascii")
local CLEANUP_MODEL = false -- if you are using the low memory GPU, you could use this flag.
local CACHE_DIR = path.join(ROOT, "cache")
local MAX_NOISE_IMAGE = 2560 * 2560 local MAX_NOISE_IMAGE = 2560 * 2560
local MAX_SCALE_IMAGE = 1280 * 1280 local MAX_SCALE_IMAGE = 1280 * 1280
local CURL_OPTIONS = { local CURL_OPTIONS = {
request_timeout = 15, request_timeout = 60,
connect_timeout = 10, connect_timeout = 60,
allow_redirects = true, allow_redirects = true,
max_redirects = 2 max_redirects = 2
} }
local CURL_MAX_SIZE = 2 * 1024 * 1024 local CURL_MAX_SIZE = 3 * 1024 * 1024
local BLOCK_OFFSET = 7 -- see srcnn.lua
local function valid_size(x, scale) local function valid_size(x, scale)
if scale == 0 then if scale == 0 then
@ -50,20 +64,16 @@ local function valid_size(x, scale)
end end
end end
local function get_image(req) local function cache_url(url)
local file = req:get_argument("file", "") local hash = md5.sumhexa(url)
local url = req:get_argument("url", "") local cache_file = path.join(CACHE_DIR, "url_" .. hash)
local blob = nil if path.exists(cache_file) then
local img = nil return image_loader.load_float(cache_file)
local alpha = nil else
if file and file:len() > 0 then
blob = file
img, alpha = image_loader.decode_float(blob)
elseif url and url:len() > 0 then
local res = coroutine.yield( local res = coroutine.yield(
turbo.async.HTTPClient({verify_ca=false}, turbo.async.HTTPClient({verify_ca=false},
nil, nil,
CURL_MAX_SIZE):fetch(url, CURL_OPTIONS) CURL_MAX_SIZE):fetch(url, CURL_OPTIONS)
) )
if res.code == 200 then if res.code == 200 then
local content_type = res.headers:get("Content-Type", true) local content_type = res.headers:get("Content-Type", true)
@ -71,33 +81,95 @@ local function get_image(req)
content_type = content_type[1] content_type = content_type[1]
end end
if content_type and content_type:find("image") then if content_type and content_type:find("image") then
blob = res.body local fp = io.open(cache_file, "wb")
img, alpha = image_loader.decode_float(blob) local blob = res.body
fp:write(blob)
fp:close()
return image_loader.decode_float(blob)
end end
end end
end end
return img, blob, alpha return nil, nil, nil
end end
local function get_image(req)
local function apply_denoise1(x) local file = req:get_argument("file", "")
return reconstruct.image(noise1_model, x, BLOCK_OFFSET) local url = req:get_argument("url", "")
if file and file:len() > 0 then
return image_loader.decode_float(file)
elseif url and url:len() > 0 then
return cache_url(url)
end
return nil, nil, nil
end end
local function apply_denoise2(x) local function cleanup_model(model)
return reconstruct.image(noise2_model, x, BLOCK_OFFSET) if CLEANUP_MODEL then
end w2nn.cleanup_model(model) -- release GPU memory
local function apply_scale2x(x)
return reconstruct.scale(scale20_model, 2.0, x, BLOCK_OFFSET)
end
local function cache_do(cache, x, func)
if path.exists(cache) then
return image.load(cache)
else
x = func(x)
image.save(cache, x)
return x
end end
end end
local function convert(x, alpha, options)
local cache_file = path.join(CACHE_DIR, options.prefix .. ".png")
local alpha_cache_file = path.join(CACHE_DIR, options.alpha_prefix .. ".png")
local alpha_orig = alpha
if path.exists(alpha_cache_file) then
alpha = image_loader.load_float(alpha_cache_file)
if alpha:dim() == 2 then
alpha = alpha:reshape(1, alpha:size(1), alpha:size(2))
end
if alpha:size(1) == 3 then
alpha = image.rgb2y(alpha)
end
end
if path.exists(cache_file) then
x = image_loader.load_float(cache_file)
return x, alpha
else
if options.style == "art" then
if options.border then
x = alpha_util.make_border(x, alpha_orig, reconstruct.offset_size(art_scale2_model))
end
if options.method == "scale" then
x = reconstruct.scale(art_scale2_model, 2.0, x)
if alpha then
if not (alpha:size(2) == x:size(2) and alpha:size(3) == x:size(3)) then
alpha = reconstruct.scale(art_scale2_model, 2.0, alpha)
image_loader.save_png(alpha_cache_file, alpha)
end
end
cleanup_model(art_scale2_model)
elseif options.method == "noise1" then
x = reconstruct.image(art_noise1_model, x)
cleanup_model(art_noise1_model)
else -- options.method == "noise2"
x = reconstruct.image(art_noise2_model, x)
cleanup_model(art_noise2_model)
end
else -- photo
if options.border then
x = alpha_util.make_border(x, alpha, reconstruct.offset_size(photo_scale2_model))
end
if options.method == "scale" then
x = reconstruct.scale(photo_scale2_model, 2.0, x)
if alpha then
if not (alpha:size(2) == x:size(2) and alpha:size(3) == x:size(3)) then
alpha = reconstruct.scale(photo_scale2_model, 2.0, alpha)
image_loader.save_png(alpha_cache_file, alpha)
end
end
cleanup_model(photo_scale2_model)
elseif options.method == "noise1" then
x = reconstruct.image(photo_noise1_model, x)
cleanup_model(photo_noise1_model)
elseif options.method == "noise2" then
x = reconstruct.image(photo_noise2_model, x)
cleanup_model(photo_noise2_model)
end
end
image_loader.save_png(cache_file, x)
return x, alpha
end
end
local function client_disconnected(handler) local function client_disconnected(handler)
return not(handler.request and return not(handler.request and
handler.request.connection and handler.request.connection and
@ -112,63 +184,63 @@ function APIHandler:post()
self:write("client disconnected") self:write("client disconnected")
return return
end end
local x, src, alpha = get_image(self) local x, alpha, blob = get_image(self)
local scale = tonumber(self:get_argument("scale", "0")) local scale = tonumber(self:get_argument("scale", "0"))
local noise = tonumber(self:get_argument("noise", "0")) local noise = tonumber(self:get_argument("noise", "0"))
local style = self:get_argument("style", "art")
local download = (self:get_argument("download", "")):len()
if style ~= "art" then
style = "photo" -- style must be art or photo
end
if x and valid_size(x, scale) then if x and valid_size(x, scale) then
if USE_CACHE and (noise ~= 0 or scale ~= 0) then if (noise ~= 0 or scale ~= 0) then
local hash = md5.sumhexa(src) local hash = md5.sumhexa(blob)
local cache_noise1 = path.join(CACHE_DIR, hash .. "_noise1.png") local alpha_prefix = style .. "_" .. hash .. "_alpha"
local cache_noise2 = path.join(CACHE_DIR, hash .. "_noise2.png") local border = false
local cache_scale = path.join(CACHE_DIR, hash .. "_scale.png") if scale ~= 0 and alpha then
local cache_noise1_scale = path.join(CACHE_DIR, hash .. "_noise1_scale.png") border = true
local cache_noise2_scale = path.join(CACHE_DIR, hash .. "_noise2_scale.png") end
if noise == 1 then if noise == 1 then
x = cache_do(cache_noise1, x, apply_denoise1) x = convert(x, alpha, {method = "noise1", style = style,
prefix = style .. "_noise1_" .. hash,
alpha_prefix = alpha_prefix, border = border})
border = false
elseif noise == 2 then elseif noise == 2 then
x = cache_do(cache_noise2, x, apply_denoise2) x = convert(x, alpha, {method = "noise2", style = style,
prefix = style .. "_noise2_" .. hash,
alpha_prefix = alpha_prefix, border = border})
border = false
end end
if scale == 1 or scale == 2 then if scale == 1 or scale == 2 then
local prefix
if noise == 1 then if noise == 1 then
x = cache_do(cache_noise1_scale, x, apply_scale2x) prefix = style .. "_noise1_scale_" .. hash
elseif noise == 2 then elseif noise == 2 then
x = cache_do(cache_noise2_scale, x, apply_scale2x) prefix = style .. "_noise2_scale_" .. hash
else else
x = cache_do(cache_scale, x, apply_scale2x) prefix = style .. "_scale_" .. hash
end end
x, alpha = convert(x, alpha, {method = "scale", style = style, prefix = prefix, alpha_prefix = alpha_prefix, border = border})
if scale == 1 then if scale == 1 then
x = iproc.scale(x, x = iproc.scale(x, x:size(3) * (1.6 / 2.0), x:size(2) * (1.6 / 2.0), "Sinc")
math.floor(x:size(3) * (1.6 / 2.0) + 0.5),
math.floor(x:size(2) * (1.6 / 2.0) + 0.5),
"Jinc")
end end
end end
elseif noise ~= 0 or scale ~= 0 then
if noise == 1 then
x = apply_denoise1(x)
elseif noise == 2 then
x = apply_denoise2(x)
end
if scale == 1 then
local x16 = {math.floor(x:size(3) * 1.6 + 0.5), math.floor(x:size(2) * 1.6 + 0.5)}
x = apply_scale2x(x)
x = iproc.scale(x, x16[1], x16[2], "Jinc")
elseif scale == 2 then
x = apply_scale2x(x)
end
end end
local name = uuid() .. ".png" local name = uuid() .. ".png"
local blob, len = image_loader.encode_png(x, alpha) local blob = image_loader.encode_png(alpha_util.composite(x, alpha))
self:set_header("Content-Disposition", string.format('filename="%s"', name)) self:set_header("Content-Disposition", string.format('filename="%s"', name))
self:set_header("Content-Type", "image/png") self:set_header("Content-Length", string.format("%d", #blob))
self:set_header("Content-Length", string.format("%d", len)) if download > 0 then
self:write(ffi.string(blob, len)) self:set_header("Content-Type", "application/octet-stream")
else
self:set_header("Content-Type", "image/png")
end
self:write(blob)
else else
if not x then if not x then
self:set_status(400) self:set_status(400)
self:write("ERROR: unsupported image format.") self:write("ERROR: An error occurred. (unsupported image format/connection timeout/file is too large)")
else else
self:set_status(400) self:set_status(400)
self:write("ERROR: image size exceeds maximum allowable size.") self:write("ERROR: image size exceeds maximum allowable size.")
@ -177,9 +249,10 @@ function APIHandler:post()
collectgarbage() collectgarbage()
end end
local FormHandler = class("FormHandler", turbo.web.RequestHandler) local FormHandler = class("FormHandler", turbo.web.RequestHandler)
local index_ja = file.read("./assets/index.ja.html") local index_ja = file.read(path.join(ROOT, "assets", "index.ja.html"))
local index_ru = file.read("./assets/index.ru.html") local index_ru = file.read(path.join(ROOT, "assets", "index.ru.html"))
local index_en = file.read("./assets/index.html") local index_pt = file.read(path.join(ROOT, "assets", "index.pt.html"))
local index_en = file.read(path.join(ROOT, "assets", "index.html"))
function FormHandler:get() function FormHandler:get()
local lang = self.request.headers:get("Accept-Language") local lang = self.request.headers:get("Accept-Language")
if lang then if lang then
@ -191,6 +264,8 @@ function FormHandler:get()
self:write(index_ja) self:write(index_ja)
elseif langs[1] == "ru" then elseif langs[1] == "ru" then
self:write(index_ru) self:write(index_ru)
elseif langs[1] == "pt" or langs[1] == "pt-BR" then
self:write(index_pt)
else else
self:write(index_en) self:write(index_en)
end end
@ -209,10 +284,8 @@ turbo.log.categories = {
local app = turbo.web.Application:new( local app = turbo.web.Application:new(
{ {
{"^/$", FormHandler}, {"^/$", FormHandler},
{"^/index.html", turbo.web.StaticFileHandler, path.join("./assets", "index.html")},
{"^/index.ja.html", turbo.web.StaticFileHandler, path.join("./assets", "index.ja.html")},
{"^/index.ru.html", turbo.web.StaticFileHandler, path.join("./assets", "index.ru.html")},
{"^/api$", APIHandler}, {"^/api$", APIHandler},
{"^/([%a%d%.%-_]+)$", turbo.web.StaticFileHandler, path.join(ROOT, "assets/")},
} }
) )
app:listen(opt.port, "0.0.0.0", {max_body_size = CURL_MAX_SIZE}) app:listen(opt.port, "0.0.0.0", {max_body_size = CURL_MAX_SIZE})

14
webgen/README.md Normal file
View file

@ -0,0 +1,14 @@
# webgen
## Generating web pages
```
ruby gen.rb
```
View at `../assets`.
## Adding a translation file
1. Adding a translation file to `./locales`
2. Run `./gen.rb`

BIN
webgen/assets/bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

BIN
webgen/assets/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

28
webgen/assets/mobile.css Normal file
View file

@ -0,0 +1,28 @@
body {
width: 98%;
font-size: 100%;
}
.all-page {
width: auto;
margin: 1em auto;
padding: 1em;
}
.main-title {
display: block;
}
.option-left {
width: auto;
display: block;
}
#url {
width: 100%;
height: 2em;
}
.option-right {
display: block;
}
.button {
min-width: 10px;
width: 100%;
height: 3em;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

View file

@ -0,0 +1 @@
chibi_20162765420.png was generated by http://tetrabo.com/chibichara/

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

191
webgen/assets/style.css Normal file
View file

@ -0,0 +1,191 @@
button::-moz-focus-inner,
input[type="reset"]::-moz-focus-inner,
input[type="button"]::-moz-focus-inner,
input[type="submit"]::-moz-focus-inner,
input[type="submit"]::-moz-focus-inner,
input[type="file"] > input[type="button"]::-moz-focus-inner
{
border: none;
}
input[type="checkbox"]:focus {
-moz-outline-offset: -1px !important;
-moz-outline: 1px solid #000 !important;
}
:focus {
outline: none;
} /*Remove a dotted line around 1) buttons, 2) checkboxes, 3) links*/
a {
text-decoration: none;
cursor: pointer;
color: inherit;
}
a:hover {
text-decoration: underline;
}
div, span, a, input {
background-repeat: no-repeat;
}
body {
width: 782px;
margin: 0 auto;
background: #ccc url(bg.png) no-repeat center bottom;
color: #000;
font-size: 14px;
font-family: Tahoma, Arial, Verdana, Meiryo, "MS Gothic", sans-serif, Lucida Sans;
line-height: 1.5em;
text-align: center;
}
.all-page {
position: relative;
width: 690px;
margin: 15px auto;
padding: 10px 30px 15px 30px;
background: #eee;
border: 2px solid #999;
border-radius: 8px;
text-align: left;
}
.all-page:after {
content: "";
position: absolute;
left: -1px;
top: -1px;
width: 100%;
height: 100%;
height: calc(100% - 2px);
padding: 0 1px;
box-shadow: 0px 5px 8px #bbb;
z-index: -1;
} /*for crop shadow bottom for 4px (2px from border and 2px from calc)*/
.main-title {
font-size: 2em;
font-weight: bold;
margin: 0.6em 0;
white-space: nowrap;
display: inline-block;
}
.choose-lang {
font-size: 0.8em;
margin: 0 5px;
opacity: 0.9;
vertical-align: middle;
}
p {
margin: 0.4em 0;
}
p.margin1 { margin: 0.9em 0; }
.links-box {
color: #999;
}
.example {
width: 445px;
height: 200px;
}
.blue-link {
color: #36b;
}
.gray-link {
color: #999;
}
.second-title {
font-size: 1.5em;
font-weight: bold;
margin: 1em 0 1em;
line-height: 1.3em;
}
.option-box {
margin: 1.5em 0;
white-space: nowrap;
}
.option-left {
display: inline-block;
width: 180px;
color: #707070;
font-weight: bold;
}
.option-left-small {
font-size: 0.8em;
line-height: 1.5em;
}
.option-right {
display: inline-block;
white-space: normal;
vertical-align: top;
}
.option-right-small {
margin-top: 2px;
font-size: 0.9em;
}
.option-hint {
margin: 0.5em 0;
color: #888;
font-size: 0.85em;
line-height: 1.5em;
white-space: normal;
}
#url {
width: 300px;
height: 23px;
padding: 0 3px;
border: 1px solid #b0b0b0;
}
label {
margin: 0 5px 0 0;
padding: 0;
cursor: pointer;
}
.radio {
margin: 0 4px 0 0;
padding: 0;
cursor: pointer;
vertical-align: middle;
}
.r-text {
vertical-align: middle;
}
.radio:checked + .r-text { color: #494; }
.button {
min-width: 160px;
height: 26px;
margin: 0 10px 3px 0;
padding-bottom: 1px;
background: #f2f2f2;
background-image: linear-gradient(to bottom, #f9f9f9, #dadada);
border: 1px solid #999;
border-radius: 1px;
cursor: pointer;
}
.button:hover {
background: #f7f7f7;
background-image: linear-gradient(to bottom, #fefefe, #e2e2e2);
}
.bottom-hint {
margin: 0.85em 0;
color: #888;
font-size: 0.85em;
line-height: 1.5em;
text-align: center;
}

52
webgen/assets/ui.js Normal file
View file

@ -0,0 +1,52 @@
$(function (){
var expires = 365;
function clear_file() {
var new_file = $("#file").clone();
new_file.change(clear_url);
$("#file").replaceWith(new_file);
}
function clear_url() {
$("#url").val("")
}
function on_change_style(e) {
var checked = $("input[name=style]:checked");
if (checked.val() == "art") {
$(".main-title").text("waifu2x");
} else {
$(".main-title").html("w<s>/a/</s>ifu2x");
}
$.cookie("style", checked.val(), {expires: expires});
}
function on_change_noise_level(e)
{
var checked = $("input[name=noise]:checked");
$.cookie("noise", checked.val(), {expires: expires});
}
function on_change_scale_factor(e)
{
var checked = $("input[name=scale]:checked");
$.cookie("scale", checked.val(), {expires: expires});
}
function restore_from_cookie()
{
if ($.cookie("style")) {
$("input[name=style]").filter("[value=" + $.cookie("style") + "]").prop("checked", true)
}
if ($.cookie("noise")) {
$("input[name=noise]").filter("[value=" + $.cookie("noise") + "]").prop("checked", true)
}
if ($.cookie("scale")) {
$("input[name=scale]").filter("[value=" + $.cookie("scale") + "]").prop("checked", true)
}
}
$("#url").change(clear_file);
$("#file").change(clear_url);
$("input[name=style]").change(on_change_style);
$("input[name=noise]").change(on_change_noise_level);
$("input[name=scale]").change(on_change_scale_factor);
restore_from_cookie();
on_change_style();
on_change_scale_factor();
on_change_noise_level();
})

60
webgen/gen.rb Normal file
View file

@ -0,0 +1,60 @@
require 'erb'
require 'yaml'
require 'optparse'
require 'fileutils'
def to_h(a)
Hash[a]
end
def symbolize_keys(val)
if val.is_a?(Hash)
to_h(val.map{|k,v|
[k.to_sym, symbolize_keys(v)]
})
elsif val.is_a?(Array)
val = val.map{|v| symbolize_keys(v)}
else
val
end
end
def load_locales(dir)
locales = {}
Dir.entries(dir).each do |ent|
if ent =~ /^\w\w.yml$/
lang = File.basename(ent, ".yml")
yml = YAML.load_file(File.join(dir, ent))
if yml
locales[lang.to_sym] = symbolize_keys(yml)
else
locales[lang.to_sym] = {}
end
end
end
locales
end
def copy(indir, outdir)
files = Dir.entries(indir).to_a.map{|ent|
File.join(indir, ent)
}.select{|ent|
File.file?(ent)
}
FileUtils.copy(files, outdir, preserve: true)
end
DIR = File.dirname(__FILE__)
DEFAULT_LANG = :en
LANG_DIR = File.join(DIR, "locales")
OUTPUT_DIR = File.join(DIR, "..", "assets")
DONT_MAKE_CHANGE = "This file was automatically generated by webgen/gen.rb. Do not make changes to this file manually."
locales = load_locales(LANG_DIR)
template = File.read(File.join(DIR, "templates", "index.html.erb"))
erb = ERB.new(template)
locales.each do |lang, locale|
output_path = File.join(OUTPUT_DIR, lang == DEFAULT_LANG ? "index.html" : "index.#{lang}.html")
t = locales[DEFAULT_LANG].merge(locale)
t[:dont_make_change] = DONT_MAKE_CHANGE
t[:lang] = lang.to_s
File.write(output_path, erb.result(binding))
end
copy(File.join(DIR, "assets"), OUTPUT_DIR)

24
webgen/locales/en.yml Normal file
View file

@ -0,0 +1,24 @@
---
description: Single-Image Super-Resolution for Anime-Style Art using Deep Convolutional Neural Networks. And it supports photo.
show_demonstration: Show full demonstration
go_to_github: Go to GitHub
image_choosing: Image choosing
type_url: Type URL
choose_file: Or choose a file
file_limits: "Limits: Size: 3MB, Noise Reduction: 2560x2560px, Upscaling: 1280x1280px."
style: Style
artwork: Artwork
photo: Photo
noise_reduction: Noise Reduction
expect_jpeg: expect JPEG artifact
nr_none: None
nr_medium: Medium
nr_high: High
nr_hint: "You need use noise reduction if image actually has noise or it may cause opposite effect."
upscaling: Upscaling
up_none: None
button_convert: Convert
button_download: Download
hints:
- "If you are using Firefox, Please press the CTRL+S key to save image. \"Save Image\" option doesn't work."

23
webgen/locales/ja.yml Normal file
View file

@ -0,0 +1,23 @@
---
description: 深層畳み込みニューラルネットワークによる二次元画像のための超解像システム。 写真にも対応。
show_demonstration: 実行例を表示
go_to_github: プロジェクトページ(GitHub)
image_choosing: 画像を選択
type_url: URLを入力
choose_file: ファイルを選択
file_limits: "制限: サイズ: 3MB, ノイズ除去: 2560x2560px, 拡大(前): 1280x1280px."
style: スタイル
artwork: イラスト
photo: 写真
noise_reduction: ノイズ除去
expect_jpeg: JPEGイズを想定
nr_none: なし
nr_medium:
nr_high:
nr_hint: "イズ除去は細部が消えることがあります。JPEGイズがある場合に使用します。"
upscaling: 拡大
up_none: なし
button_convert: 実行
button_download: 実行結果を保存
hints:
- "Firefoxの方は、右クリから画像が保存できないようなので、CTRL+SキーかALTキー後 ファイル - ページを保存 で画像を保存してください。"

23
webgen/locales/pt.yml Normal file
View file

@ -0,0 +1,23 @@
---
description: Single-Image Super-Resolution for Anime-Style Art using Deep Convolutional Neural Networks. And it supports photo.
show_demonstration: Sobre
go_to_github: Ir para Github
image_choosing: Imagem
type_url: URL
choose_file: ARQUIVO
file_limits: "Limites: Tamanho: 3MB, Redução de ruído: 2560x2560px, Aumento de escala: 1280x1280px."
style: Estilo
artwork: Arte
photo: Foto
noise_reduction: Redução de ruído
expect_jpeg: Exceto artefato JPEG
nr_none: Nenhuma
nr_medium: Média
nr_high: Alta
nr_hint: "Quando usando a escala 2x, Nós nunca recomendamos usar um nível alto de redução de ruído, quase sempre deixa a imagem pior, faz sentido apenas para casos raros quando a imagem tinha uma qualidade muito má desde o começo."
upscaling: Aumento de escala
up_none: Nenhum
button_convert: Converter
button_download: Baixar
hints:
- "Se Você estiver usando o Firefox, por favor, aperte CTRL+S para salvar a imagem. A opção \"Salvar Imagem\" não funciona"

23
webgen/locales/ru.yml Normal file
View file

@ -0,0 +1,23 @@
---
description: "Waifu2x позволяет увеличивать в 4 раза рисованные изображения, например аниме или арт, а также устранять шум на изображении (преимущественно артефакты сжатия JPEG). Теперь также поддерживаются фотографии."
show_demonstration: Посмотреть полную демонстрацию
go_to_github: Перейти на GitHub
image_choosing: Выбор изображения
type_url: Укажите URL
choose_file: Либо выберите файл
file_limits: "Макс. размер файла — 3MB, устранение шума — макс. 2560x2560px, апскейл — 1280x1280px."
style: Тип изображения
artwork: Арт
photo: Фотография
noise_reduction: Устранение шума
expect_jpeg: артефактов JPEG
nr_none: Нет
nr_medium: Средне
nr_high: Сильно
nr_hint: "Устранение шума нужно использовать, если на картинке действительно есть шум, иначе это даст противоположный эффект."
upscaling: Апскейл
up_none: Нет
button_convert: Преобразовать
button_download: Скачать
hints:
- "Если Вы используете Firefox, для сохранения изображения нажмите Ctrl+S (перетаскивание изображения и опция \"Сохранить изображение\" работать не будут)."

View file

@ -0,0 +1,148 @@
<!DOCTYPE html>
<html lang="<%= t[:lang] %>">
<!-- <%= t[:dont_make_change] %> -->
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta charset="utf-8">
<link rel="shortcut icon" href="favicon.ico"/>
<meta name="viewport" content="initial-scale=1.0,width=device-width">
<link href="//cdnjs.cloudflare.com/ajax/libs/normalize/3.0.3/normalize.min.css" rel="stylesheet" type="text/css">
<link href="style.css" rel="stylesheet" type="text/css">
<link href="mobile.css" rel="stylesheet" type="text/css" media="screen and (max-width: 768px) and (min-width: 0px)">
<script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/jquery-cookie/1.4.1/jquery.cookie.js"></script>
<script type="text/javascript" src="ui.js"></script>
<title>waifu2x</title>
</head>
<body>
<div class="all-page">
<h1 class="main-title">waifu2x</h1>
<div class="choose-lang">
<a href="index.html">
English
</a>
/
<a href="index.ja.html">
日本語
</a>
/
<a href="index.ru.html">
Русский
</a>
/
<a href="index.pt.html">
Português
</a>
</div>
<p><%= t[:description] %></p>
<p class="margin1 link-box">
<a href="https://raw.githubusercontent.com/nagadomi/waifu2x/master/images/slide.png" class="blue-link" target="_blank">
<%= t[:show_demonstration] %>
</a>
|
<a href="https://github.com/nagadomi/waifu2x" class="blue-link" target="_blank">
<%= t[:go_to_github] %>
</a>
</p>
<form action="/api" method="POST" enctype="multipart/form-data" target="_blank">
<div class="option-box first">
<div class="option-left"><%= t[:image_choosing] %>:</div>
<div class="option-right">
<input type="text" id="url" name="url" placeholder="<%= t[:type_url] %>">
<div class="option-right-small">
<%= t[:choose_file] %>:
<input type="file" id="file" name="file"></div>
</div>
<div class="option-hint">
<%= t[:file_limits] %>
</div>
</div>
<div class="option-box">
<div class="option-left">
<%= t[:style] %>:
</div>
<div class="option-right">
<label><input type="radio" name="style" class="radio" value="art" checked>
<span class="r-text">
<%= t[:artwork] %>
</span>
</label>
<label><input type="radio" name="style" class="radio" value="photo">
<span class="r-text">
<%= t[:photo] %>
</span>
</label>
</div>
</div>
<div class="option-box">
<div class="option-left">
<%= t[:noise_reduction] %>:
<div class="option-left-small">
(<%= t[:expect_jpeg] %>)
</div>
</div>
<div class="option-right">
<label><input type="radio" name="noise" class="radio" value="0">
<span class="r-text">
<%= t[:nr_none] %>
</span>
</label>
<label><input type="radio" name="noise" class="radio" value="1" checked>
<span class="r-text">
<%= t[:nr_medium] %>
</span>
</label>
<label>
<input type="radio" name="noise" class="radio" value="2">
<span class="r-text">
<%= t[:nr_high] %>
</span>
</label>
</div>
<div class="option-hint">
<%= t[:nr_hint] %>
</div>
</div>
<div class="option-box">
<div class="option-left">
<%= t[:upscaling] %>:
<div class="option-left-small"></div>
</div>
<div class="option-right">
<label><input type="radio" name="scale" class="radio" value="0" checked>
<span class="r-text">
<%= t[:up_none] %>
</span>
</label>
<label><input type="radio" name="scale" class="radio" value="1">
<span class="r-text">
1.6x
</span>
</label>
<label><input type="radio" name="scale" class="radio" value="2">
<span class="r-text">
2x
</span>
</label>
</div>
</div>
<% if t[:button_convert] && !t[:button_convert].empty? %>
<input type="submit" class="button" value="<%= t[:button_convert] %>">
<% else %>
<input type="submit" class="button">
<% end %>
<input type="submit" name="download" value="<%= t[:button_download]%>" class="button">
<div class="bottom-hint">
<ul>
<% t[:hints].each do |hint| %>
<li><%= hint %></li>
<% end %>
</ul>
</div>
</form>
</div>
<div class="bottom-info">
<a href="https://github.com/nagadomi/waifu2x" class="gray-link" target="_blank">waifu2x</a>
</div>
</body>
</html>