├── .gitignore ├── Anime4KPython └── Anime4K.py ├── LICENSE ├── README.md └── example.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # Picture 132 | *.png 133 | *.jpg 134 | *.jpeg 135 | *.gif -------------------------------------------------------------------------------- /Anime4KPython/Anime4K.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | 3 | 4 | class Anime4K(object): 5 | def __init__( 6 | self, passes=2, strengthColor=1 / 3, strengthGradient=1, fastMode=False, 7 | ): 8 | # Passes for processing 9 | self.ps = passes 10 | # the range of strengthColor from 0 to 1, greater for thinner linework 11 | self.sc = strengthColor 12 | # the range of strengthGradient from 0 to 1, greater for sharper 13 | self.sg = strengthGradient 14 | # Faster but may be low quality 15 | self.fm = fastMode 16 | 17 | def loadImage(self, path="./Anime4K/pic/p1.png"): 18 | self.srcFile = path 19 | self.orgImg = cv2.imread(path, cv2.IMREAD_UNCHANGED) 20 | self.dstImg = cv2.resize( 21 | self.orgImg, dsize=None, fx=2, fy=2, interpolation=cv2.INTER_CUBIC 22 | ) 23 | self.H = self.dstImg.shape[0] 24 | self.W = self.dstImg.shape[1] 25 | 26 | # Process anime4k 27 | def process(self): 28 | for i in range(self.ps): 29 | self.getGray() 30 | self.pushColor() 31 | self.getGradient() 32 | self.pushGradient() 33 | 34 | # getGray compute the grayscale of the image and store it to the Alpha channel 35 | def getGray(self): 36 | B, G, R, A = 0, 1, 2, 3 37 | 38 | def callBack(i, j, pixel): 39 | pixel[A] = 0.299 * pixel[R] + 0.587 * pixel[G] + 0.114 * pixel[B] 40 | return pixel 41 | 42 | self.changeEachPixel(self.dstImg, callBack) 43 | 44 | # pushColor will make the linework of the image thinner guided by the grayscale in Alpha channel 45 | def pushColor(self): 46 | B, G, R, A = 0, 1, 2, 3 47 | 48 | def getLightest(mc, a, b, c): 49 | mc[R] = mc[R] * (1 - self.sc) + (a[R] / 3 + b[R] / 3 + c[R] / 3) * self.sc 50 | mc[G] = mc[G] * (1 - self.sc) + (a[G] / 3 + b[G] / 3 + c[G] / 3) * self.sc 51 | mc[B] = mc[B] * (1 - self.sc) + (a[B] / 3 + b[B] / 3 + c[B] / 3) * self.sc 52 | mc[A] = mc[A] * (1 - self.sc) + (a[A] / 3 + b[A] / 3 + c[A] / 3) * self.sc 53 | 54 | def callBack(i, j, pixel): 55 | iN, iP, jN, jP = -1, 1, -1, 1 56 | if i == 0: 57 | iN = 0 58 | elif i == self.H - 1: 59 | iP = 0 60 | if j == 0: 61 | jN = 0 62 | elif j == self.W - 1: 63 | jP = 0 64 | 65 | tl, tc, tr = ( 66 | self.dstImg[i + iN, j + jN], 67 | self.dstImg[i + iN, j], 68 | self.dstImg[i + iN, j + jP], 69 | ) 70 | ml, mc, mr = ( 71 | self.dstImg[i, j + jN], 72 | pixel, 73 | self.dstImg[i, j + jP], 74 | ) 75 | bl, bc, br = ( 76 | self.dstImg[i + iP, j + jN], 77 | self.dstImg[i + iP, j], 78 | self.dstImg[i + iP, j + jP], 79 | ) 80 | 81 | # top and bottom 82 | maxD = max(bl[A], bc[A], br[A]) 83 | minL = min(tl[A], tc[A], tr[A]) 84 | if minL > mc[A] and mc[A] > maxD: 85 | getLightest(mc, tl, tc, tr) 86 | else: 87 | maxD = max(tl[A], tc[A], tr[A]) 88 | minL = min(bl[A], bc[A], br[A]) 89 | if minL > mc[A] and mc[A] > maxD: 90 | getLightest(mc, bl, bc, br) 91 | 92 | # subdiagonal 93 | maxD = max(ml[A], mc[A], bc[A]) 94 | minL = min(tc[A], tr[A], mr[A]) 95 | if minL > maxD: 96 | getLightest(mc, tc, tr, mr) 97 | else: 98 | maxD = max(tc[A], mc[A], mr[A]) 99 | minL = min(ml[A], bl[A], bc[A]) 100 | if minL > maxD: 101 | getLightest(mc, ml, bl, bc) 102 | 103 | # left and right 104 | maxD = max(tl[A], ml[A], bl[A]) 105 | minL = min(tr[A], mr[A], br[A]) 106 | if minL > mc[A] and mc[A] > maxD: 107 | getLightest(mc, tr, mr, br) 108 | else: 109 | maxD = max(tr[A], mr[A], br[A]) 110 | minL = min(tl[A], ml[A], bl[A]) 111 | if minL > mc[A] and mc[A] > maxD: 112 | getLightest(mc, tl, ml, bl) 113 | 114 | # diagonal 115 | maxD = max(tc[A], mc[A], ml[A]) 116 | minL = min(mr[A], br[A], bc[A]) 117 | if minL > maxD: 118 | getLightest(mc, mr, br, bc) 119 | else: 120 | maxD = max(bc[A], mc[A], mr[A]) 121 | minL = min(ml[A], tl[A], tc[A]) 122 | if minL > maxD: 123 | getLightest(mc, ml, tl, tc) 124 | 125 | return pixel 126 | 127 | self.changeEachPixel(self.dstImg, callBack) 128 | 129 | # getGradient compute the gradient of the image and store it to the Alpha channel 130 | def getGradient(self): 131 | B, G, R, A = 0, 1, 2, 3 132 | if self.fm == True: 133 | 134 | def callBack(i, j, pixel): 135 | if i == 0 or j == 0 or i == self.H - 1 or j == self.W - 1: 136 | return pixel 137 | 138 | Grad = abs( 139 | self.dstImg[i + 1, j - 1][A] 140 | + 2 * self.dstImg[i + 1, j][A] 141 | + self.dstImg[i + 1, j + 1][A] 142 | - self.dstImg[i - 1, j - 1][A] 143 | - 2 * self.dstImg[i - 1, j][A] 144 | - self.dstImg[i - 1, j + 1][A] 145 | ) + abs( 146 | self.dstImg[i - 1, j - 1][A] 147 | + 2 * self.dstImg[i, j - 1][A] 148 | + self.dstImg[i + 1, j - 1][A] 149 | - self.dstImg[i - 1, j + 1][A] 150 | - 2 * self.dstImg[i, j + 1][A] 151 | - self.dstImg[i + 1, j + 1][A] 152 | ) 153 | 154 | rst = self.unFloat(Grad / 2) 155 | pixel[A] = 255 - rst 156 | return pixel 157 | 158 | else: 159 | 160 | def callBack(i, j, pixel): 161 | if i == 0 or j == 0 or i == self.H - 1 or j == self.W - 1: 162 | return pixel 163 | 164 | Grad = ( 165 | ( 166 | self.dstImg[i + 1, j - 1][A] 167 | + 2 * self.dstImg[i + 1, j][A] 168 | + self.dstImg[i + 1, j + 1][A] 169 | - self.dstImg[i - 1, j - 1][A] 170 | - 2 * self.dstImg[i - 1, j][A] 171 | - self.dstImg[i - 1, j + 1][A] 172 | ) 173 | ** 2 174 | + ( 175 | self.dstImg[i - 1, j - 1][A] 176 | + 2 * self.dstImg[i, j - 1][A] 177 | + self.dstImg[i + 1, j - 1][A] 178 | - self.dstImg[i - 1, j + 1][A] 179 | - 2 * self.dstImg[i, j + 1][A] 180 | - self.dstImg[i + 1, j + 1][A] 181 | ) 182 | ** 2 183 | ) ** (0.5) 184 | 185 | rst = self.unFloat(Grad) 186 | pixel[A] = 255 - rst 187 | return pixel 188 | 189 | self.changeEachPixel(self.dstImg, callBack) 190 | 191 | # pushGradient will make the linework of the image sharper guided by the gradient in Alpha channel 192 | def pushGradient(self): 193 | B, G, R, A = 0, 1, 2, 3 194 | 195 | def getLightest(mc, a, b, c): 196 | mc[R] = mc[R] * (1 - self.sg) + (a[R] / 3 + b[R] / 3 + c[R] / 3) * self.sg 197 | mc[G] = mc[G] * (1 - self.sg) + (a[G] / 3 + b[G] / 3 + c[G] / 3) * self.sg 198 | mc[B] = mc[B] * (1 - self.sg) + (a[B] / 3 + b[B] / 3 + c[B] / 3) * self.sg 199 | mc[A] = 255 200 | return mc 201 | 202 | def callBack(i, j, pixel): 203 | iN, iP, jN, jP = -1, 1, -1, 1 204 | if i == 0: 205 | iN = 0 206 | elif i == self.H - 1: 207 | iP = 0 208 | if j == 0: 209 | jN = 0 210 | elif j == self.W - 1: 211 | jP = 0 212 | 213 | tl, tc, tr = ( 214 | self.dstImg[i + iN, j + jN], 215 | self.dstImg[i + iN, j], 216 | self.dstImg[i + iN, j + jP], 217 | ) 218 | ml, mc, mr = ( 219 | self.dstImg[i, j + jN], 220 | pixel, 221 | self.dstImg[i, j + jP], 222 | ) 223 | bl, bc, br = ( 224 | self.dstImg[i + iP, j + jN], 225 | self.dstImg[i + iP, j], 226 | self.dstImg[i + iP, j + jP], 227 | ) 228 | 229 | # top and bottom 230 | maxD = max(bl[A], bc[A], br[A]) 231 | minL = min(tl[A], tc[A], tr[A]) 232 | if minL > mc[A] and mc[A] > maxD: 233 | return getLightest(mc, tl, tc, tr) 234 | 235 | maxD = max(tl[A], tc[A], tr[A]) 236 | minL = min(bl[A], bc[A], br[A]) 237 | if minL > mc[A] and mc[A] > maxD: 238 | return getLightest(mc, bl, bc, br) 239 | 240 | # subdiagonal 241 | maxD = max(ml[A], mc[A], bc[A]) 242 | minL = min(tc[A], tr[A], mr[A]) 243 | if minL > maxD: 244 | return getLightest(mc, tc, tr, mr) 245 | maxD = max(tc[A], mc[A], mr[A]) 246 | minL = min(ml[A], bl[A], bc[A]) 247 | if minL > maxD: 248 | return getLightest(mc, ml, bl, bc) 249 | 250 | # left and right 251 | maxD = max(tl[A], ml[A], bl[A]) 252 | minL = min(tr[A], mr[A], br[A]) 253 | if minL > mc[A] and mc[A] > maxD: 254 | return getLightest(mc, tr, mr, br) 255 | maxD = max(tr[A], mr[A], br[A]) 256 | minL = min(tl[A], ml[A], bl[A]) 257 | if minL > mc[A] and mc[A] > maxD: 258 | return getLightest(mc, tl, ml, bl) 259 | 260 | # diagonal 261 | maxD = max(tc[A], mc[A], ml[A]) 262 | minL = min(mr[A], br[A], bc[A]) 263 | if minL > maxD: 264 | return getLightest(mc, mr, br, bc) 265 | maxD = max(bc[A], mc[A], mr[A]) 266 | minL = min(ml[A], tl[A], tc[A]) 267 | if minL > maxD: 268 | return getLightest(mc, ml, tl, tc) 269 | 270 | pixel[A] = 255 271 | return pixel 272 | 273 | self.changeEachPixel(self.dstImg, callBack) 274 | 275 | # show the dstImg 276 | def show(self): 277 | cv2.imshow("dstImg", self.dstImg) 278 | cv2.waitKey() 279 | 280 | # changeEachPixel will traverse all the pixel of the image, and change it by callBack function, all the change will be applied after traversing 281 | def changeEachPixel(self, img, callBack): 282 | tmp = img.copy() 283 | for i in range(self.H): 284 | for j in range(self.W): 285 | tmp[i, j] = callBack(i, j, img[i, j].copy()) 286 | self.dstImg = tmp 287 | 288 | # unFloat convert float to uint8,range from 0-255 289 | def unFloat(self, n): 290 | n += 0.5 291 | if n >= 255: 292 | return 255 293 | elif n <= 0: 294 | return 0 295 | return n 296 | 297 | # ShowInfo will show the basic infomation of the image 298 | def showInfo(self): 299 | print("Width: %d, Height: %d" % (self.orgImg.shape[1], self.orgImg.shape[0])) 300 | print("----------------------------------------------") 301 | print( 302 | "Input: %s\nPasses: %d\nFast Mode: %r\nStrength color: %g\nStrength gradient: %g" 303 | % (self.srcFile, self.ps, self.fm, self.sc, self.sg) 304 | ) 305 | print("----------------------------------------------") 306 | 307 | # save image to disk 308 | def saveImage(self, filename="./output.png"): 309 | cv2.imwrite(filename, self.dstImg) 310 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 TianZer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Anime4KPython 2 | This is an implementation of Anime4K in Python. It based on the [bloc97's Anime4K](https://github.com/bloc97/Anime4K) algorithm version 0.9 and some optimizations have been made. 3 | This project is for learning and the exploration task of algorithm course in SWJTU. 4 | ***NOTICE: The Python version will be very slow. It is about 100 times slower than [Go version](https://github.com/TianZerL/Anime4KGo), because it take too much time to traverse the image. So it just suitable for learning how the Anime4K works. If you want to use anime4k in python, see [pyanime4k](https://github.com/TianZerL/pyanime4k).*** 5 | 6 | # pyanime4k 7 | [pyanime4k](https://github.com/TianZerL/pyanime4k) is a simply package to use anime4k in python, easy, fast and powerful, which support both image and video processing, based on [Anime4KCPP](https://github.com/TianZerL/Anime4KCPP). 8 | 9 | # About Anime4K 10 | Anime4K is a simple high-quality anime upscale algorithm for anime. it does not use any machine learning approaches, and can be very fast in real-time processing. 11 | 12 | # Usage 13 | Please install opencv-python before using. 14 | 15 | pip install opencv-python 16 | 17 | See the example.py, it is very easy to use. 18 | 19 | There are four main arguments: 20 | - **passes**: Passes for processing. 21 | - **strengthColor**: Strength for pushing color,range 0 to 1,higher for thinner 22 | - **strengthGradient**: Strength for pushing gradient,range 0 to 1,higher for sharper 23 | - **fastMode**: Faster but maybe lower quality 24 | 25 | # Other implementations 26 | - Go 27 | - [TianZerL/Anime4KGo](https://github.com/TianZerL/Anime4KGo) 28 | - C++ 29 | - [TianZerL/Anime4KCPP](https://github.com/TianZerL/Anime4KCPP) 30 | - C# 31 | - [shadow578/Anime4kSharp](https://github.com/shadow578/Anime4kSharp) 32 | - [net2cn/Anime4KSharp](https://github.com/net2cn/Anime4KSharp) 33 | - Java 34 | - [bloc97/Anime4K](https://github.com/bloc97/Anime4K) 35 | - Rust 36 | - [andraantariksa/Anime4K-rs](https://github.com/andraantariksa/Anime4K-rs) -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | from Anime4KPython.Anime4K import Anime4K 2 | import time 3 | 4 | if __name__ == "__main__": 5 | #give arguments 6 | anime4k = Anime4K(passes=2,strengthColor=1/3,strengthGradient=1,fastMode=False) 7 | #load your image 8 | anime4k.loadImage("../Anime4K/pic/p1.png") 9 | #show basic infomation 10 | anime4k.showInfo() 11 | time_start = time.time() 12 | #main process 13 | anime4k.process() 14 | time_end = time.time() 15 | print("Total time:", time_end - time_start, "s") 16 | #show thr result by opencv 17 | anime4k.show() 18 | #save to disk 19 | anime4k.saveImage("../Anime4K/output.png") 20 | --------------------------------------------------------------------------------