浏览代码

Merge pull request #1 from nagadomi/master

Update branch
Álex G.M 9 年之前
父节点
当前提交
cb2d4386c2
共有 94 个文件被更改,包括 4214 次插入1262 次删除
  1. 2 0
      .gitattributes
  2. 13 1
      .gitignore
  3. 1 0
      NOTICE
  4. 41 23
      README.md
  5. 9 1
      appendix/purge_cache.lua
  6. 二进制
      assets/bg.png
  7. 二进制
      assets/favicon.ico
  8. 136 80
      assets/index.html
  9. 136 80
      assets/index.ja.html
  10. 146 0
      assets/index.pt.html
  11. 136 80
      assets/index.ru.html
  12. 28 0
      assets/mobile.css
  13. 191 0
      assets/style.css
  14. 52 0
      assets/ui.js
  15. 33 34
      convert_data.lua
  16. 0 34
      cudnn2cunn.lua
  17. 0 23
      export_model.lua
  18. 1 2
      images/gen.sh
  19. 二进制
      images/lena_waifu2x.png
  20. 二进制
      images/lena_waifu2x_ukbench.png
  21. 二进制
      images/miku_CC_BY-NC_noisy_waifu2x.png
  22. 二进制
      images/miku_noisy_waifu2x.png
  23. 二进制
      images/miku_small.png
  24. 二进制
      images/miku_small_lanczos3.png
  25. 二进制
      images/miku_small_noisy_waifu2x.png
  26. 二进制
      images/miku_small_waifu2x.png
  27. 二进制
      images/slide.odp
  28. 二进制
      images/slide.png
  29. 二进制
      images/slide_noise_reduction.png
  30. 二进制
      images/slide_result.png
  31. 二进制
      images/slide_upscaling.png
  32. 39 0
      lib/ClippedWeightedHuberCriterion.lua
  33. 77 0
      lib/DepthExpand2x.lua
  34. 4 3
      lib/LeakyReLU.lua
  35. 31 0
      lib/LeakyReLU_deprecated.lua
  36. 25 0
      lib/WeightedMSECriterion.lua
  37. 80 0
      lib/alpha_util.lua
  38. 4 24
      lib/cleanup_model.lua
  39. 17 0
      lib/compression.lua
  40. 124 0
      lib/data_augmentation.lua
  41. 81 49
      lib/image_loader.lua
  42. 128 5
      lib/iproc.lua
  43. 18 22
      lib/minibatch_adam.lua
  44. 217 245
      lib/pairwise_transform.lua
  45. 0 4
      lib/portable.lua
  46. 125 20
      lib/reconstruct.lua
  47. 60 39
      lib/settings.lua
  48. 58 50
      lib/srcnn.lua
  49. 26 0
      lib/w2nn.lua
  50. 16 34
      models/anime_style_art/noise1_model.t7
  51. 16 34
      models/anime_style_art/noise2_model.t7
  52. 16 34
      models/anime_style_art/scale2.0x_model.t7
  53. 0 0
      models/anime_style_art_rgb/noise1_model.json
  54. 33 27
      models/anime_style_art_rgb/noise1_model.t7
  55. 0 0
      models/anime_style_art_rgb/noise2_model.json
  56. 33 27
      models/anime_style_art_rgb/noise2_model.t7
  57. 0 0
      models/anime_style_art_rgb/scale2.0x_model.json
  58. 33 27
      models/anime_style_art_rgb/scale2.0x_model.t7
  59. 0 0
      models/photo/noise1_model.json
  60. 161 0
      models/photo/noise1_model.t7
  61. 0 0
      models/photo/noise2_model.json
  62. 161 0
      models/photo/noise2_model.t7
  63. 0 0
      models/photo/scale2.0x_model.json
  64. 161 0
      models/photo/scale2.0x_model.t7
  65. 0 0
      models/ukbench/scale2.0x_model.json
  66. 33 27
      models/ukbench/scale2.0x_model.t7
  67. 209 0
      tools/benchmark.lua
  68. 25 0
      tools/cleanup_model.lua
  69. 43 0
      tools/cudnn2cunn.lua
  70. 43 0
      tools/cunn2cudnn.lua
  71. 54 0
      tools/export_model.lua
  72. 43 0
      tools/rebuild_model.lua
  73. 180 89
      train.lua
  74. 8 6
      train.sh
  75. 12 0
      train_photo.sh
  76. 9 0
      train_ukbench.sh
  77. 130 42
      waifu2x.lua
  78. 169 96
      web.lua
  79. 14 0
      webgen/README.md
  80. 二进制
      webgen/assets/bg.png
  81. 二进制
      webgen/assets/favicon.ico
  82. 28 0
      webgen/assets/mobile.css
  83. 二进制
      webgen/assets/src/chibi_20162765420.png
  84. 1 0
      webgen/assets/src/chibi_20162765420.txt
  85. 二进制
      webgen/assets/src/chibi_2x.png
  86. 二进制
      webgen/assets/src/favicon.psd
  87. 191 0
      webgen/assets/style.css
  88. 52 0
      webgen/assets/ui.js
  89. 60 0
      webgen/gen.rb
  90. 24 0
      webgen/locales/en.yml
  91. 23 0
      webgen/locales/ja.yml
  92. 23 0
      webgen/locales/pt.yml
  93. 23 0
      webgen/locales/ru.yml
  94. 148 0
      webgen/templates/index.html.erb

+ 2 - 0
.gitattributes

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

+ 13 - 1
.gitignore

@@ -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 - 0
NOTICE

@@ -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/).

+ 41 - 23
README.md

@@ -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
-AMI NAME: waifu2x-server
-Instance Type: g2.2xlarge
-Region: US West (N.California)
-OS: Ubuntu 14.04
-User: ubuntu
-Created at: 2015-08-12
+TODO
 ```
 ```
 
 
 ## 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)
 
 
-#### Validation
-
-Test the waifu2x command line tool.
+And install luarocks packages.
 ```
 ```
-th waifu2x.lua
+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
 ```
 ```
 
 
-### Setting Up the Web Application Environment (if you needed)
+#### Getting waifu2x
 
 
-#### Install packages
+```
+git clone --depth 1 https://github.com/nagadomi/waifu2x.git
+```
+
+#### Validation
 
 
+Testing the waifu2x command line tool.
 ```
 ```
-luarocks install md5
-luarocks install uuid
-PREFIX=$HOME/torch/install luarocks install turbo
+th waifu2x.lua
 ```
 ```
 
 
 ## 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.
 ```
 ```

+ 9 - 1
appendix/purge_cache.lua

@@ -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

二进制
assets/bg.png


二进制
assets/favicon.ico


+ 136 - 80
assets/index.html

@@ -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">
-    <link rel="canonical" href="http://waifu2x.udp.jp/">
+    <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>
     <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="header">
-      <div style="position:absolute; display:block; top:0; left:540px; max-height:140px;">
-        <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="https://github.com/nagadomi/waifu2x" target="_blank" style="position:absolute; display:block; left:0; top:0; width:149px; height:130px;"></a>
+    <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>
       </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>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>
-    </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 class="bottom-info">
+      <a href="https://github.com/nagadomi/waifu2x" class="gray-link" target="_blank">waifu2x</a>
     </div>
     </div>
   </body>
   </body>
 </html>
 </html>

+ 136 - 80
assets/index.ja.html

@@ -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">
-    <link rel="canonical" href="http://waifu2x.udp.jp/">
+    <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>
     <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="header">
-      <div style="position:absolute; display:block; top:0; left:540px; max-height:140px;">
-        <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="https://github.com/nagadomi/waifu2x" target="_blank" style="position:absolute; display:block; left:0; top:0; width:149px; height:130px;"></a>
+    <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>
       </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>深層畳み込みニューラルネットワークによる二次元画像のための超解像システム. <a href="https://raw.githubusercontent.com/nagadomi/waifu2x/master/images/slide.png" target="_blank">about</a>.</div>
-    </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 class="bottom-info">
+      <a href="https://github.com/nagadomi/waifu2x" class="gray-link" target="_blank">waifu2x</a>
     </div>
     </div>
   </body>
   </body>
 </html>
 </html>

+ 146 - 0
assets/index.pt.html

@@ -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>

+ 136 - 80
assets/index.ru.html

@@ -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">
-    <link rel="canonical" href="http://waifu2x.udp.jp/">
+    <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>
     <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="header">
-      <div style="position:absolute; display:block; top:0; left:540px; max-height:140px;">
-        <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="https://github.com/nagadomi/waifu2x" target="_blank" style="position:absolute; display:block; left:0; top:0; width:149px; height:130px;"></a>
+    <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>
       </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>Увеличение в 4 раза рисованных изображений, например, аниме или фан-арт, а также устранение шума (преимущественно артефактов сжатия JPEG), см. <a href="https://raw.githubusercontent.com/nagadomi/waifu2x/master/images/slide.png" target="_blank">демонстрацию и сравнения</a></div>
-    </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 class="bottom-info">
+      <a href="https://github.com/nagadomi/waifu2x" class="gray-link" target="_blank">waifu2x</a>
     </div>
     </div>
   </body>
   </body>
 </html>
 </html>

+ 28 - 0
assets/mobile.css

@@ -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 - 0
assets/style.css

@@ -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 - 0
assets/ui.js

@@ -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();
+})

+ 33 - 34
convert_data.lua

@@ -1,48 +1,47 @@
-require './lib/portable'
-require 'image'
-local settings = require './lib/settings'
-local image_loader = require './lib/image_loader'
-
-local function count_lines(file)
-   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 __FILE__ = (function() return string.gsub(debug.getinfo(2, 'S').source, "^@", "") end)()
+package.path = path.join(path.dirname(__FILE__), "lib", "?.lua;") .. package.path
 
 
-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
+require 'pl'
+require 'image'
+local compression = require 'compression'
+local settings = require 'settings'
+local image_loader = require 'image_loader'
+local iproc = require 'iproc'
 
 
 local function load_images(list)
 local function load_images(list)
-   local count = count_lines(list)
-   local fp = io.open(list, "r")
+   local MARGIN = 32
+   local lines = utils.split(file.read(list), "\n")
    local x = {}
    local x = {}
-   local c = 0
-   for line in fp:lines() do
-      local im = crop_4x(image_loader.load_byte(line))
-      if im then
-	 if im:size(2) >= settings.crop_size * 2 and im:size(3) >= settings.crop_size * 2 then
-	    table.insert(x, im)
-	 end
+   for i = 1, #lines do
+      local line = lines[i]
+      local im, alpha = image_loader.load_byte(line)
+      if alpha then
+	 io.stderr:write(string.format("\n%s: skip: image has alpha channel.\n", line))
       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(c, count)
-      if c % 10 == 0 then
+      xlua.progress(i, #lines)
+      if i % 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)
-

+ 0 - 34
cudnn2cunn.lua

@@ -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)

+ 0 - 23
export_model.lua

@@ -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))

+ 1 - 2
images/gen.sh

@@ -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

二进制
images/lena_waifu2x.png


二进制
images/lena_waifu2x_ukbench.png


二进制
images/miku_CC_BY-NC_noisy_waifu2x.png


二进制
images/miku_noisy_waifu2x.png


二进制
images/miku_small.png


二进制
images/miku_small_lanczos3.png


二进制
images/miku_small_noisy_waifu2x.png


二进制
images/miku_small_waifu2x.png


二进制
images/slide.odp


二进制
images/slide.png


二进制
images/slide_noise_reduction.png


二进制
images/slide_result.png


二进制
images/slide_upscaling.png


+ 39 - 0
lib/ClippedWeightedHuberCriterion.lua

@@ -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 - 0
lib/DepthExpand2x.lua

@@ -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

+ 4 - 3
lib/LeakyReLU.lua

@@ -1,7 +1,8 @@
-if nn.LeakyReLU then
-   return
+if w2nn and w2nn.LeakyReLU then
+   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)

+ 31 - 0
lib/LeakyReLU_deprecated.lua

@@ -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

+ 25 - 0
lib/WeightedMSECriterion.lua

@@ -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 - 0
lib/alpha_util.lua

@@ -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

+ 4 - 24
cleanup_model.lua → lib/cleanup_model.lua

@@ -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
-
-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
+function w2nn.cleanup_model(model)
    cleanupModel(model)
    cleanupModel(model)
-   torch.save(opt.model, model, opt.oformat)
-else
-   error("model not found")
+   return model
 end
 end

+ 17 - 0
lib/compression.lua

@@ -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 - 0
lib/data_augmentation.lua

@@ -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

+ 81 - 49
lib/image_loader.lua

@@ -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 im, alpha = image_loader.decode_byte(blob)
-   if im then
-      im = im:float():div(255)
-   end
-   return im, alpha
-end
-function image_loader.encode_png(rgb, alpha)
-   if rgb:type() == "torch.ByteTensor" then
-      error("expect FloatTensor")
-   end
-   if alpha then
-      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)
+local clip_eps8 = (1.0 / 255.0) * 0.5 - (1.0e-7 * (1.0 / 255.0) * 0.5)
+local clip_eps16 = (1.0 / 65535.0) * 0.5 - (1.0e-7 * (1.0 / 65535.0) * 0.5)
+local background_color = 0.5
+
+function image_loader.encode_png(rgb, depth)
+   depth = depth or 8
+   rgb = iproc.byte2float(rgb)
+   if depth < 16 then
+      rgb = rgb:clone():add(clip_eps8)
+      rgb[torch.lt(rgb, 0.0)] = 0.0
+      rgb[torch.gt(rgb, 1.0)] = 1.0
+      rgb = rgb:mul(255):long():float():div(255)
    else
    else
-      local im = gm.Image(rgb, "RGB", "DHW")
-      im:format("png")
-      return im:toBlob(9)
+      rgb = rgb:clone():add(clip_eps16)
+      rgb[torch.lt(rgb, 0.0)] = 0.0
+      rgb[torch.gt(rgb, 1.0)] = 1.0
+      rgb = rgb:mul(65535):long():float():div(65535)
+   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
    end
+   return im:depth(depth):format("PNG"):toString(9)
 end
 end
-function image_loader.save_png(filename, rgb, alpha)
-   local blob, len = image_loader.encode_png(rgb, alpha)
+function image_loader.save_png(filename, rgb, depth)
+   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()
-	 if sum_alpha > 0 or sum_alpha < 0 then
+	 local sum_alpha = (im[4] - 1.0):sum()
+	 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'
-   local img
-   img = image_loader.load_float("./a.jpg")
-   if img then
-      print(img:min())
-      print(img:max())
-      image.display(img)
-   end
-   img = image_loader.load_float("./b.png")
-   if img then
-      image.display(img)
-   end
+   torch.setdefaulttensortype("torch.FloatTensor")
+   local a = image_loader.load_float("../images/lena.png")
+   local blob = image_loader.encode_png(a)
+   local b = image_loader.decode_float(blob)
+   assert((b - a):abs():sum() == 0)
+
+   a = image_loader.load_byte("../images/lena.png")
+   blob = image_loader.encode_png(a)
+   b = image_loader.decode_byte(blob)
+   assert((b:float() - a:float()):abs():sum() == 0)
 end
 end
 --test()
 --test()
 return image_loader
 return image_loader

+ 128 - 5
lib/iproc.lua

@@ -1,16 +1,83 @@
 local gm = require 'graphicsmagick'
 local gm = require 'graphicsmagick'
 local image = require 'image'
 local image = require 'image'
+
 local iproc = {}
 local iproc = {}
+local clip_eps8 = (1.0 / 255.0) * 0.5 - (1.0e-7 * (1.0 / 255.0) * 0.5)
 
 
-function iproc.scale(src, width, height, filter)
-   local t = "float"
+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
+   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
    if src:type() == "torch.ByteTensor" then
-      t = "byte"
+      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
    end
+   return dest, conversion
+end
+function iproc.scale(src, width, height, filter)
+   local conversion, color
+   src, conversion = iproc.byte2float(src)
    filter = filter or "Box"
    filter = filter or "Box"
-   local im = gm.Image(src, "RGB", "DHW")
+   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)
    im:size(math.ceil(width), math.ceil(height), filter)
-   return im:toTensor(t, "RGB", "DHW")
+   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"
+   local im = gm.Image(src, "RGB", "DHW")
+   im:gammaCorrection(1.0 / 2.2):
+      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

+ 18 - 22
lib/minibatch_adam.lua

@@ -3,38 +3,35 @@ require 'cutorch'
 require 'xlua'
 require 'xlua'
 
 
 local function minibatch_adam(model, criterion,
 local function minibatch_adam(model, criterion,
-			      train_x,
-			      config, transformer,
-			      input_size, target_size)
+			      train_x, train_y,
+			      config)
    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])
-   
-   for t = 1, #train_x, batch_size do
-      if t + batch_size > #train_x then
+				    train_y:size(2)):zero()
+   local inputs = inputs_tmp:clone():cuda()
+   local targets = targets_tmp:clone():cuda()
+
+   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(x)
-	 targets_tmp[i]:copy(y)
+         inputs_tmp[i]:copy(train_x[shuffle[t + i - 1]])
+	 targets_tmp[i]:copy(train_y[shuffle[t + i - 1]])
       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

+ 217 - 245
lib/pairwise_transform.lua

@@ -1,291 +1,263 @@
 require 'image'
 require 'image'
 local gm = require 'graphicsmagick'
 local gm = require 'graphicsmagick'
-local iproc = require './iproc'
-local reconstruct = require './reconstruct'
+local iproc = require 'iproc'
+local data_augmentation = require 'data_augmentation'
+
 local pairwise_transform = {}
 local pairwise_transform = {}
 
 
-local function random_half(src, p, min_size)
-   p = p or 0.5
-   local filter = ({"Box","Blackman", "SincFast", "Jinc"})[torch.random(1, 4)]
-   if p > torch.uniform() then
+local function random_half(src, p)
+   if torch.uniform() < p then
+      local filter = ({"Box","Box","Blackman","Sinc","Lanczos", "Catrom"})[torch.random(1, 6)]
       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 color_scale = torch.Tensor(3):uniform(0.8, 1.2)
-   x = x:float():div(255)
-   for i = 1, 3 do
-      x[i]:mul(color_scale[i])
-   end
-   x[torch.lt(x, 0.0)] = 0.0
-   x[torch.gt(x, 1.0)] = 1.0
-   return x:mul(255):byte()
-end
-local function flip_augment(x, y)
-   local flip = torch.random(1, 4)
-   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
+local function crop_if_large(src, max_size)
+   local tries = 4
+   if src:size(2) > max_size and src:size(3) > max_size then
+      local rect
+      for i = 1, tries do
+	 local yi = torch.random(0, src:size(2) - max_size)
+	 local xi = torch.random(0, src:size(3) - max_size)
+	 rect = iproc.crop(src, xi, yi, xi + max_size, yi + max_size)
+	 -- ignore simple background
+	 if rect:float():std() >= 0 then
+	    break
+	 end
       end
       end
-      return x, y
+      return rect
    else
    else
-      if flip == 1 then
-	 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
+      return src
    end
    end
 end
 end
-local INTERPOLATION_PADDING = 16
-function pairwise_transform.scale(src, scale, size, offset, options)
-   options = options or {color_augment = true, random_half = true, rgb = true}
-   if options.random_half then
-      src = random_half(src)
-   end
-   local yi = torch.random(INTERPOLATION_PADDING, src:size(2) - size - INTERPOLATION_PADDING)
-   local xi = torch.random(INTERPOLATION_PADDING, src:size(3) - size - INTERPOLATION_PADDING)
-   local down_scale = 1.0 / scale
-   local y = image.crop(src,
-			xi - INTERPOLATION_PADDING, yi - INTERPOLATION_PADDING,
-			xi + size + INTERPOLATION_PADDING, yi + size + INTERPOLATION_PADDING)
-   local filters = {
-      "Box",        -- 0.012756949974688
-      "Blackman",   -- 0.013191924552285
-      --"Cartom",     -- 0.013753536746706
-      --"Hanning",    -- 0.013761314529647
-      --"Hermite",    -- 0.013850225205266
-      "SincFast",   -- 0.014095824314306
-      "Jinc",       -- 0.014244299255442
-   }
-   local downscale_filter = filters[torch.random(1, #filters)]
+local function preprocess(src, crop_size, options)
+   local dest = src
+   dest = random_half(dest, options.random_half_rate)
+   dest = crop_if_large(dest, math.max(crop_size * 2, options.max_size))
+   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)
    
    
-   y = flip_augment(y)
-   if options.color_augment then
-      y = color_augment(y)
+   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 x = iproc.scale(y, y:size(3) * down_scale, y:size(2) * down_scale, downscale_filter)
-   x = iproc.scale(x, y:size(3), y:size(2))
-   y = y:float():div(255)
-   x = x:float():div(255)
-
-   if options.rgb then
+   if p < r then
+      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 yc = iproc.crop(y, xi, yi, xi + size, yi + size)
+      return xc, yc
    else
    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))
+      local lowres = gm.Image(x, "RGB", "DHW"):
+	 size(x:size(3) * 0.5, x:size(2) * 0.5, "Box"):
+	 size(x:size(3), x:size(2), "Box"):
+	 toTensor(t, "RGB", "DHW")
+      local best_se = 0.0
+      local best_xc, best_yc
+      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
-
-   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)
-   options = options or {color_augment = true, random_half = true, rgb = true}
-   if options.random_half then
-      src = random_half(src)
-   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
+function pairwise_transform.scale(src, scale, size, offset, n, options)
+   local filters;
 
 
-   if options.color_augment then
-      y = color_augment(y)
-   end
-   x = y
-   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
-   
-   y = image.crop(y, xi, yi, xi + size, yi + size)
-   x = image.crop(x, xi, yi, xi + size, yi + size)
-   y = y:float():div(255)
-   x = x:float():div(255)
-   x, y = flip_augment(x, y)
-   
-   if options.rgb then
+   if options.style == "photo" then
+      filters = {
+	 "Box", "lanczos", "Catrom"
+      }
    else
    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))
+      filters = {
+	 "Box","Box",  -- 0.012756949974688
+	 "Blackman",   -- 0.013191924552285
+	 --"Catrom",     -- 0.013753536746706
+	 --"Hanning",    -- 0.013761314529647
+	 --"Hermite",    -- 0.013850225205266
+	 "Sinc",   -- 0.014095824314306
+	 "Lanczos",       -- 0.014244299255442
+      }
    end
    end
+   local unstable_region_offset = 8
+   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))
    
    
-   return x, image.crop(y, offset, offset, size - offset, size - offset)
-end
-function pairwise_transform.jpeg(src, level, size, offset, options)
-   if level == 1 then
-      return pairwise_transform.jpeg_(src, {torch.random(65, 85)},
-				      size, offset,
-				      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)
+   local batch = {}
+   for i = 1, n do
+      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
       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)
+	 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
       end
-   else
-      error("unknown noise level: " .. level)
+      table.insert(batch, {xc, iproc.crop(yc, offset, offset, size - offset, size - offset)})
    end
    end
+   return batch
 end
 end
-function pairwise_transform.jpeg_scale_(src, scale, quality, size, offset, options)
-   if options.random_half then
-      src = random_half(src)
-   end
-   local down_scale = 1.0 / scale
-   local filters = {
-      "Box",        -- 0.012756949974688
-      "Blackman",   -- 0.013191924552285
-      --"Cartom",     -- 0.013753536746706
-      --"Hanning",    -- 0.013761314529647
-      --"Hermite",    -- 0.013850225205266
-      "SincFast",   -- 0.014095824314306
-      "Jinc",       -- 0.014244299255442
-   }
-   local downscale_filter = filters[torch.random(1, #filters)]
-   local yi = torch.random(INTERPOLATION_PADDING, src:size(2) - size - INTERPOLATION_PADDING)
-   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)
+function pairwise_transform.jpeg_(src, quality, size, offset, n, options)
+   local unstable_region_offset = 8
+   local y = preprocess(src, size, options)
+   local 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:samplingFactors({1.0, 1.0, 1.0})
+      x:format("jpeg"):depth(8)
+      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.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
+   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))
    
    
-   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)
+   local batch = {}
+   for i = 1, n do
+      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
       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)
+	 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
-   else
-      error("unknown noise level: " .. level)
    end
    end
+   return batch
 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())
+function pairwise_transform.jpeg(src, style, level, size, offset, n, options)
+   if style == "art" then
+      if level == 1 then
+	 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)
+   else
+      error("unknown style: " .. style)
    end
    end
 end
 end
 
 
-local function test_scale()
-   local loader = require './image_loader'
-   local src = loader.load_byte("../images/miku_CC_BY-NC.jpg")   
+function pairwise_transform.test_jpeg(src)
+   torch.setdefaulttensortype("torch.FloatTensor")
+   local options = {random_color_noise_rate = 0.5,
+		    random_half_rate = 0.5,
+		    random_overlay_rate = 0.5,
+		    random_unsharp_mask_rate = 0.5,
+		    jpeg_chroma_subsampling_rate = 0.5,
+		    nr_rate = 1.0,
+		    active_cropping_rate = 0.5,
+		    active_cropping_tries = 10,
+		    max_size = 256,
+		    rgb = true
+   }
+   local image = require 'image'
+   local src = image.lena()
    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})
-      image.display({image = y, legend = "y:" .. (i * 10), min = 0, max = 1})
-      image.display({image = x, legend = "x:" .. (i * 10), min = 0, max = 1})
-      print(y:size(), x:size())
-      --print(x:mean(), y:mean())
+      local xy = pairwise_transform.jpeg(src,
+					 "art",
+					 torch.random(1, 2),
+					 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
 end
 end
-local function test_jpeg_scale()
-   local loader = require './image_loader'
-   local src = loader.load_byte("../images/miku_CC_BY-NC.jpg")   
-   for i = 1, 9 do
-      local y, x = pairwise_transform.jpeg_scale(src, 2.0, 1, 128, 7, {color_augment = true, random_half = true})
-      image.display({image = y, legend = "y1:" .. (i * 10), min = 0, max = 1})
-      image.display({image = x, legend = "x1:" .. (i * 10), min = 0, max = 1})
-      print(y:size(), x:size())
-      --print(x:mean(), y:mean())
-   end
-   for i = 1, 9 do
-      local y, x = pairwise_transform.jpeg_scale(src, 2.0, 2, 128, 7, {color_augment = true, random_half = true})
-      image.display({image = y, legend = "y2:" .. (i * 10), min = 0, max = 1})
-      image.display({image = x, legend = "x2:" .. (i * 10), min = 0, max = 1})
-      print(y:size(), x:size())
-      --print(x:mean(), y:mean())
+function pairwise_transform.test_scale(src)
+   torch.setdefaulttensortype("torch.FloatTensor")
+   local options = {random_color_noise_rate = 0.5,
+		    random_half_rate = 0.5,
+		    random_overlay_rate = 0.5,
+		    random_unsharp_mask_rate = 0.5,
+		    active_cropping_rate = 0.5,
+		    active_cropping_tries = 10,
+		    max_size = 256,
+		    rgb = true
+   }
+   local image = require 'image'
+   local src = image.lena()
+
+   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
 end
 end
---test_scale()
---test_jpeg()
---test_jpeg_scale()
-
 return pairwise_transform
 return pairwise_transform

+ 0 - 4
lib/portable.lua

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

+ 125 - 20
lib/reconstruct.lua

@@ -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
-
-local reconstruct = {}
+function reconstruct.offset_size(model)
+   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)
-   local output = image.yuv2rgb(image.crop(yuv_jinc,
+   yuv_lanczos[1]:copy(y)
+   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)
-   if model_is_rgb(model) then
-      return reconstruct.image_rgb(model, x, offset, block_size)
+function reconstruct.image(model, 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.image_rgb(model, x,
+				reconstruct.offset_size(model), block_size)
+   else
+      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
    else
-      return reconstruct.image_y(model, x, offset, block_size)
+      x = reconstruct.scale_y(model, scale, x,
+			      reconstruct.offset_size(model), block_size)
+   end
+   if i2rgb then
+      x = image.rgb2y(x)
    end
    end
+   return x
 end
 end
-function reconstruct.scale(model, scale, x, offset, block_size)
-   if model_is_rgb(model) then
-      return reconstruct.scale_rgb(model, scale, x, offset, block_size)
+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
+function reconstruct.scale_tta(model, scale, x, block_size)
+   if reconstruct.is_rgb(model) then
+      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
 
 

+ 60 - 39
lib/settings.lua

@@ -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("-data_dir", "./data", 'data directory')
-cmd:option("-test", "images/miku_small.png", 'test image file')
+cmd:option("-gpu", -1, 'GPU Device ID')
+cmd:option("-seed", 11, 'RNG seed')
+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("-learning_rate", 0.00025, 'learning rate for adam')
-cmd:option("-random_half", 1, 'enable data augmentation using half resolution image')
-cmd:option("-crop_size", 128, 'crop size')
-cmd:option("-batch_size", 2, 'mini batch size')
-cmd:option("-epoch", 200, 'epoch')
-cmd:option("-core", 2, 'cpu core')
+cmd:option("-random_color_noise_rate", 0.0, 'data augmentation using color noise (0.0-1.0)')
+cmd:option("-random_overlay_rate", 0.0, 'data augmentation using flipped image overlay (0.0-1.0)')
+cmd:option("-random_half_rate", 0.0, 'data augmentation using half resolution image (0.0-1.0)')
+cmd:option("-random_unsharp_mask_rate", 0.0, 'data augmentation using unsharp mask (0.0-1.0)')
+cmd:option("-scale", 2.0, 'scale factor (2)')
+cmd:option("-learning_rate", 0.0005, 'learning rate for adam')
+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
-   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)
-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)
+if settings.save_history == 1 then
+   settings.save_history = true
 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
-   settings.random_half = true
-else
-   settings.random_half = false
+if not (settings.style == "art" or
+	settings.style == "photo") then
+   error(string.format("unknown style: %s", settings.style))
+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

+ 58 - 50
lib/srcnn.lua

@@ -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 = {}
-function srcnn.waifu2x(color)
-   local model = nn.Sequential()
-   local ch = nil
-   if color == "rgb" then
-      ch = 3
-   elseif color == "y" then
-      ch = 1
-   else
-      if color then
-	 error("unknown color: " .. color)
-      else
-	 error("unknown color: nil")
-      end
+if cudnn and cudnn.SpatialConvolution then
+   function cudnn.SpatialConvolution:reset(stdv)
+      stdv = math.sqrt(2 / ((1.0 + 0.1 * 0.1) * self.kW * self.kH * self.nOutputPlane))
+      self.weight:normal(0, stdv)
+      self.bias:zero()
    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()
---print(model:forward(torch.Tensor(32, 1, 92, 92):uniform():cuda()):size())
+   --model:cuda()
+   --print(model:forward(torch.Tensor(32, ch, 92, 92):uniform():cuda()):size())
    
    
-   return model, 7
+   return model
 end
 end
-
--- current 4x is worse than 2x * 2
-function srcnn.waifu4x(color)
+function srcnn.waifu2x_cudnn(ch)
    local model = nn.Sequential()
    local model = nn.Sequential()
-
-   local ch = nil
+   model:add(cudnn.SpatialConvolution(ch, 32, 3, 3, 1, 1, 0, 0))
+   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 - 0
lib/w2nn.lua

@@ -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

文件差异内容过多而无法显示
+ 16 - 34
models/anime_style_art/noise1_model.t7


文件差异内容过多而无法显示
+ 16 - 34
models/anime_style_art/noise2_model.t7


文件差异内容过多而无法显示
+ 16 - 34
models/anime_style_art/scale2.0x_model.t7


文件差异内容过多而无法显示
+ 0 - 0
models/anime_style_art_rgb/noise1_model.json


文件差异内容过多而无法显示
+ 33 - 27
models/anime_style_art_rgb/noise1_model.t7


文件差异内容过多而无法显示
+ 0 - 0
models/anime_style_art_rgb/noise2_model.json


文件差异内容过多而无法显示
+ 33 - 27
models/anime_style_art_rgb/noise2_model.t7


文件差异内容过多而无法显示
+ 0 - 0
models/anime_style_art_rgb/scale2.0x_model.json


文件差异内容过多而无法显示
+ 33 - 27
models/anime_style_art_rgb/scale2.0x_model.t7


文件差异内容过多而无法显示
+ 0 - 0
models/photo/noise1_model.json


文件差异内容过多而无法显示
+ 161 - 0
models/photo/noise1_model.t7


文件差异内容过多而无法显示
+ 0 - 0
models/photo/noise2_model.json


文件差异内容过多而无法显示
+ 161 - 0
models/photo/noise2_model.t7


文件差异内容过多而无法显示
+ 0 - 0
models/photo/scale2.0x_model.json


文件差异内容过多而无法显示
+ 161 - 0
models/photo/scale2.0x_model.t7


文件差异内容过多而无法显示
+ 0 - 0
models/ukbench/scale2.0x_model.json


文件差异内容过多而无法显示
+ 33 - 27
models/ukbench/scale2.0x_model.t7


+ 209 - 0
tools/benchmark.lua

@@ -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 - 0
tools/cleanup_model.lua

@@ -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 - 0
tools/cudnn2cunn.lua

@@ -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 - 0
tools/cunn2cudnn.lua

@@ -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 - 0
tools/export_model.lua

@@ -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 - 0
tools/rebuild_model.lua

@@ -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)

+ 180 - 89
train.lua

@@ -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'
-local minibatch_adam = require './lib/minibatch_adam'
-local iproc = require './lib/iproc'
-local reconstruct = require './lib/reconstruct'
-local pairwise_transform = require './lib/pairwise_transform'
-local image_loader = require './lib/image_loader'
+require 'w2nn'
+local settings = require 'settings'
+local srcnn = require 'srcnn'
+local minibatch_adam = require 'minibatch_adam'
+local iproc = require 'iproc'
+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
-	 local x, y = transformer(x[i], true)
-	 table.insert(data, {x = x:reshape(1, x:size(1), x:size(2), x:size(3)),
-			     y = y:reshape(1, y:size(1), y:size(2), y:size(3))})
+      for k = 1, math.max(n / patches, 1) do
+	 local xy = transformer(x[i], true, patches)
+	 local tx = torch.Tensor(patches, xy[1][1]:size(1), xy[1][1]:size(2), xy[1][1]: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 % 10 == 0 then
+      if i % 100 == 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)
-   assert(offset == settings.block_offset)
-   local criterion = nn.MSECriterion():cuda()
+   local LR_MIN = 1.0e-5
+   local model = srcnn.create(settings.method, settings.backend, settings.color)
+   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_ratio * #x),
-				       settings.validation_crops)
-   local test = image_loader.load_float(settings.test)
+   local train_x, valid_x = split_data(x, math.floor(settings.validation_rate * #x))
    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,
-			   transformer,
-			   {ch, settings.crop_size, settings.crop_size},
-			   {ch, settings.crop_size - offset * 2, settings.crop_size - offset * 2}
-			  ))
-      model:evaluate()
-      print("# validation")
-      local score = validate(model, criterion, valid_xy)
-      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
+      resampling(x, y, train_x, pairwise_func)
+      for i = 1, settings.inner_epoch do
+	 print(minibatch_adam(model, criterion, x, y, adam_config))
+	 model:evaluate()
+	 print("# validation")
+	 local score = validate(model, criterion, valid_xy)
+	 if score < best_score then
+	    local test_image = image_loader.load_float(settings.test) -- reload
 	    lrd_count = 0
 	    lrd_count = 0
-	    adam_config.learningRate = adam_config.learningRate * 0.8
-	    print("* learning rate decay: " .. adam_config.learningRate)
+	    best_score = score
+	    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)

+ 8 - 6
train.sh

@@ -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 cleanup_model.lua -model models/anime_style_art_rgb/noise1_model.t7 -oformat ascii
+th convert_data.lua
 
 
-th train.lua -color rgb -method noise -noise_level 2 -model_dir models/anime_style_art_rgb -test images/miku_noisy.png
-th cleanup_model.lua -model models/anime_style_art_rgb/noise2_model.t7 -oformat ascii
+th train.lua -method scale -model_dir models/anime_style_art_rgb -test images/miku_small.png -thread 4
+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 cleanup_model.lua -model models/anime_style_art_rgb/scale2.0x_model.t7 -oformat ascii
+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 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 - 0
train_photo.sh

@@ -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 - 0
train_ukbench.sh

@@ -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

+ 130 - 42
waifu2x.lua

@@ -1,12 +1,12 @@
-require './lib/portable'
-require 'sys'
 require 'pl'
 require 'pl'
-require './lib/LeakyReLU'
-
-local iproc = require './lib/iproc'
-local reconstruct = require './lib/reconstruct'
-local image_loader = require './lib/image_loader'
-local BLOCK_OFFSET = 7
+local __FILE__ = (function() return string.gsub(debug.getinfo(2, 'S').source, "^@", "") end)()
+package.path = path.join(path.dirname(__FILE__), "lib", "?.lua;") .. package.path
+require 'sys'
+require 'w2nn'
+local iproc = require 'iproc'
+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")
-      model:evaluate()
-      new_x = reconstruct.image(model, x, BLOCK_OFFSET, opt.crop_size)
+      local model_path = path.join(opt.model_dir, ("noise%d_model.t7"):format(opt.noise_level))
+      local model = torch.load(model_path, "ascii")
+      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")
-      model:evaluate()
-      new_x = reconstruct.scale(model, opt.scale, x, BLOCK_OFFSET, opt.crop_size)
+      local model_path = path.join(opt.model_dir, ("scale%.1fx_model.t7"):format(opt.scale))
+      local model = torch.load(model_path, "ascii")
+      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 scale_model = torch.load(path.join(opt.model_dir, ("scale%.1fx_model.t7"):format(opt.scale)), "ascii")
-      noise_model:evaluate()
-      scale_model:evaluate()
-      x = reconstruct.image(noise_model, x, BLOCK_OFFSET)
-      new_x = reconstruct.scale(scale_model, opt.scale, x, BLOCK_OFFSET, opt.crop_size)
+      local noise_model_path = path.join(opt.model_dir, ("noise%d_model.t7"):format(opt.noise_level))
+      local noise_model = torch.load(noise_model_path, "ascii")
+      local scale_model_path = path.join(opt.model_dir, ("scale%.1fx_model.t7"):format(opt.scale))
+      local scale_model = torch.load(scale_model_path, "ascii")
+      
+      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 noise2_model = torch.load(path.join(opt.model_dir, "noise2_model.t7"), "ascii")
-   local scale_model = torch.load(path.join(opt.model_dir, ("scale%.1fx_model.t7"):format(opt.scale)), "ascii")
-
-   noise1_model:evaluate()
-   noise2_model:evaluate()
-   scale_model:evaluate()
-   
+   local model_path, noise1_model, noise2_model, scale_model
+   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.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)
-	    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))
+	    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)
-	    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))
+	    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("-l", "", 'path of the image-list')
+   cmd:option("-i", "images/miku_small.png", 'path to input image')
+   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("-model_dir", "./models/anime_style_art_rgb", 'model directory')
+   cmd:option("-o", "(auto)", 'path to output file')
+   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

+ 169 - 96
web.lua

@@ -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'
-require './lib/portable'
-require './lib/LeakyReLU'
+local iproc = require 'iproc'
+local reconstruct = require 'reconstruct'
+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)
-
-local iproc = require './lib/iproc'
-local reconstruct = require './lib/reconstruct'
-local image_loader = require './lib/image_loader'
-
-local MODEL_DIR = "./models/anime_style_art_rgb"
-
-local noise1_model = torch.load(path.join(MODEL_DIR, "noise1_model.t7"), "ascii")
-local noise2_model = torch.load(path.join(MODEL_DIR, "noise2_model.t7"), "ascii")
-local scale20_model = torch.load(path.join(MODEL_DIR, "scale2.0x_model.t7"), "ascii")
-
-local USE_CACHE = true
-local CACHE_DIR = "./cache"
+if opt.thread > 0 then
+   torch.setnumthreads(opt.thread)
+end
+if cudnn then
+   cudnn.fastest = true
+   cudnn.benchmark = false
+end
+local ART_MODEL_DIR = path.join(ROOT, "models", "anime_style_art_rgb")
+local PHOTO_MODEL_DIR = path.join(ROOT, "models", "photo")
+local art_noise1_model = torch.load(path.join(ART_MODEL_DIR, "noise1_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 photo_scale2_model = torch.load(path.join(PHOTO_MODEL_DIR, "scale2.0x_model.t7"), "ascii")
+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,
-   connect_timeout = 10,
+   request_timeout = 60,
+   connect_timeout = 60,
    allow_redirects = true,
    allow_redirects = true,
    max_redirects = 2
    max_redirects = 2
 }
 }
-local CURL_MAX_SIZE = 2 * 1024 * 1024
-local BLOCK_OFFSET = 7 -- see srcnn.lua
+local CURL_MAX_SIZE = 3 * 1024 * 1024
 
 
 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 file = req:get_argument("file", "")
-   local url = req:get_argument("url", "")
-   local blob = nil
-   local img = nil
-   local alpha = nil
-   if file and file:len() > 0 then
-      blob = file
-      img, alpha = image_loader.decode_float(blob)
-   elseif url and url:len() > 0 then
+local function cache_url(url)
+   local hash = md5.sumhexa(url)
+   local cache_file = path.join(CACHE_DIR, "url_" .. hash)
+   if path.exists(cache_file) then
+      return image_loader.load_float(cache_file)
+   else
       local res = coroutine.yield(
       local res = coroutine.yield(
 	 turbo.async.HTTPClient({verify_ca=false},
 	 turbo.async.HTTPClient({verify_ca=false},
-				nil,
-				CURL_MAX_SIZE):fetch(url, CURL_OPTIONS)
+	    nil,
+	    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
-	    img, alpha = image_loader.decode_float(blob)
+	    local fp = io.open(cache_file, "wb")
+	    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
-end
-
-local function apply_denoise1(x)
-   return reconstruct.image(noise1_model, x, BLOCK_OFFSET)
+   return nil, nil, nil
 end
 end
-local function apply_denoise2(x)
-   return reconstruct.image(noise2_model, x, BLOCK_OFFSET)
+local function get_image(req)
+   local file = req:get_argument("file", "")
+   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_scale2x(x)
-   return reconstruct.scale(scale20_model, 2.0, x, BLOCK_OFFSET)
+local function cleanup_model(model)
+   if CLEANUP_MODEL then
+      w2nn.cleanup_model(model) -- release GPU memory
+   end
 end
 end
-local function cache_do(cache, x, func)
-   if path.exists(cache) then
-      return image.load(cache)
+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
    else
-      x = func(x)
-      image.save(cache, x)
-      return x
+      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
 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
-	 local hash = md5.sumhexa(src)
-	 local cache_noise1 = path.join(CACHE_DIR, hash .. "_noise1.png")
-	 local cache_noise2 = path.join(CACHE_DIR, hash .. "_noise2.png")
-	 local cache_scale = path.join(CACHE_DIR, hash .. "_scale.png")
-	 local cache_noise1_scale = path.join(CACHE_DIR, hash .. "_noise1_scale.png")
-	 local cache_noise2_scale = path.join(CACHE_DIR, hash .. "_noise2_scale.png")
-	 
+      if (noise ~= 0 or scale ~= 0) then
+	 local hash = md5.sumhexa(blob)
+	 local alpha_prefix = style .. "_" .. hash .. "_alpha"
+	 local border = false
+	 if scale ~= 0 and alpha then
+	    border = true
+	 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,
-			       math.floor(x:size(3) * (1.6 / 2.0) + 0.5),
-			       math.floor(x:size(2) * (1.6 / 2.0) + 0.5),
-			       "Jinc")
+	       x = iproc.scale(x, x:size(3) * (1.6 / 2.0), x:size(2) * (1.6 / 2.0), "Sinc")
 	    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", len))
-      self:write(ffi.string(blob, len))
+      self:set_header("Content-Length", string.format("%d", #blob))
+      if download > 0 then
+	 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_ru = file.read("./assets/index.ru.html")
-local index_en = file.read("./assets/index.html")
+local index_ja = file.read(path.join(ROOT, "assets", "index.ja.html"))
+local index_ru = file.read(path.join(ROOT, "assets", "index.ru.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 - 0
webgen/README.md

@@ -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`

二进制
webgen/assets/bg.png


二进制
webgen/assets/favicon.ico


+ 28 - 0
webgen/assets/mobile.css

@@ -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;
+}

二进制
webgen/assets/src/chibi_20162765420.png


+ 1 - 0
webgen/assets/src/chibi_20162765420.txt

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

二进制
webgen/assets/src/chibi_2x.png


二进制
webgen/assets/src/favicon.psd


+ 191 - 0
webgen/assets/style.css

@@ -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 - 0
webgen/assets/ui.js

@@ -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 - 0
webgen/gen.rb

@@ -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 - 0
webgen/locales/en.yml

@@ -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 - 0
webgen/locales/ja.yml

@@ -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 - 0
webgen/locales/pt.yml

@@ -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 - 0
webgen/locales/ru.yml

@@ -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 (перетаскивание изображения и опция \"Сохранить изображение\" работать не будут)."

+ 148 - 0
webgen/templates/index.html.erb

@@ -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>

部分文件因为文件数量过多而无法显示