MVFU (Like Kung Fu.) - Nice conversion for VX/Ace assets

Uthgard

Veteran
Veteran
Joined
Jun 2, 2014
Messages
31
Reaction score
9
First Language
Spanish
Primarily Uses
Sadly, the page on which MVFU relied to make the conversions has recently installed a captcha system, and... well, as you can see in previous posts, the application no longer works. It would be amazing if someone could figure out a work-around, but things aren't looking good in that regard.
 

Trayvonis

Villager
Member
Joined
Sep 5, 2017
Messages
5
Reaction score
2
First Language
English
Primarily Uses
RMMV
Sadly, the page on which MVFU relied to make the conversions has recently installed a captcha system, and... well, as you can see in previous posts, the application no longer works. It would be amazing if someone could figure out a work-around, but things aren't looking good in that regard.
Thank you Uthgard, I was hoping enough time had passed a work-around had been found.
 

Tuomo L

Oldbie
Veteran
Joined
Aug 6, 2012
Messages
2,403
Reaction score
1,321
First Language
Finnish
Primarily Uses
RMMV
All my characters turn totally pitch black.
 

Uthgard

Veteran
Veteran
Joined
Jun 2, 2014
Messages
31
Reaction score
9
First Language
Spanish
Primarily Uses
Seeing as this is unlikely to ever get fixed (and this isn't a criticism to Ultima2876, I really appreciate his work and effort) does anyone have any suggestions on how to get equivalent results with the desktop waifu2x app? It can be downloaded here, but I confess I don't even know how to try to get it to work half as well as Mvfu did before the whole captcha debacle.

Edit: Decided to play a bit with it instead of looking at the screen in bewilderment. It's not that hard to get acceptable results with set rate 1.5, denoise and magnify and playing around with the denoise level and model according to your needs. Of course, people with a more educated eye than I should be able to get those guesses down to something better than "meh, good enough".
 
Last edited:

Ultima2876

Veteran
Veteran
Joined
Oct 25, 2015
Messages
185
Reaction score
235
First Language
English
Hey guys,

Indeed, the captcha really messed things up. Some mobile apps in China started hitting the Waifu2x servers HARD and making a lot of cash off of it, so nagadomi decided to shut down external API requests.

I've shut down the MVFU site now as unfortunately setting it up to do its own Waifu scaling would require a GPU instance, costing a significant amount more (at least $150/month -- the old server was only costing me $5/month). I can't afford to host it as a hobby project unfortunately.

So the other solution is to put the algorithm source here and hope someone can make a desktop app or something :)

It's dirty, it's nasty, it's horrible. Enjoy!

PHP:
<?php
set_time_limit(90);

function isImageType($type) {
    global $imageType;
    return (strpos($imageType, $type) !== false);
}

                //      WIDTH           HEIGHT  TYPE
$imageTypes = array(
                        256 => array(
                                        512 =>  'tilesets_a5_vx',
                                    ),
                        288 => array(
                                        256 =>  'characters_dsplus',
                                    ),
                        384 => array(
                                        192 =>  'facesets_vx',
                                        256 =>  'characters_vx',
                                        384 =>  'icons_vx',
                                        768 =>  'tilesets_a5_mv_stylize',
                                        936 =>  'icons_ace',
                                    ),
                        512 => array(
                                        256 =>  'tilesets_a3_vx',
                                        384 =>  'tilesets_a1a2_vx',
                                        480 =>  'tilesets_a4_vx',
                                        512 =>  'tilesets_bcde_vx',
                                    ),
                        576 => array(
                                    288 =>  'facesets_mv_stylize',
                                    384 =>  'characters_mv_stylize',
                        ),
                        768 => array(
                                    384 =>  'tilesets_a3_mv_stylize',
                                    576 =>  'tilesets_a1a2_mv_stylize',
                                    720 =>  'tilesets_a4_mv_stylize',
                                    768 =>  'tilesets_bcde_mv_stylize',
                        ),
                    );

if(!isset($sessionId)) {
    $sessionId = strtolower(base_convert(intval(round(microtime(true) * 1000) . rand(1, 100000)), 10, 36) . str_replace("php", "", pathinfo($_FILES['file']['tmp_name'], PATHINFO_FILENAME)));
}

$inputFileName = $sessionId . '.png';
if(!isset($inputFilePath)) {
    $inputFileDir = '/home/kickoptr/public_html/public/mvfu/';
    $inputFilePath = $inputFileDir . $inputFileName;
}

$outputFilePath = '/home/kickoptr/public_html/public/mvfu/output/' . $inputFileName;
$tempDir = '/home/kickoptr/public_html/public/tmp/' . pathinfo($inputFileName, PATHINFO_FILENAME) . '/';

if(!file_exists($tempDir)) {
    mkdir($tempDir, 0775, true);
}

// Add a border between every sprite (if padding is on) before Waifu processing
$borderSize = 2;

$startTime = time();

$srcSize = getimagesize($inputFilePath);
$srcWidth = $srcSize[0];
$srcHeight = $srcSize[1];

if(isset($imageTypes[$srcWidth][$srcHeight])) {
    $imageType = $imageTypes[$srcWidth][$srcHeight];
} else {
    // We can force character sheets with a flag
    if($character == true) {
        $imageType = 'characters_forced';
    }

    // files with dollars or bangs are always treated as characters!
    if($bang == true) {
        if(isImageType('character')) {
            $imageType .= '_bang';
        } else $imageType = 'characters_bang';
    }
    if($dollar == true) {
        if(isImageType('character')) {
            $imageType .= '_dollar';
        } else $imageType = 'characters_dollar';
    }

    // If it's not a character by this point, error
    if(isImageType('character') == false) {
        $error = 'Incompatible file (not supported yet?)';
        removeDirectory($tempDir);
        return $error;
    }
}

$destWidth = $srcWidth;
$destHeight = $srcHeight;
if(isImageType('stylize')) {
    $destWidth = intval($srcWidth * 2/3);
    $destHeight = intval($srcHeight * 2/3);
}

// Load src image
$srcImage=imagecreatetruecolor($destWidth, $destHeight);
$alpha = imagecolorallocatealpha($srcImage, 0, 0, 0, 127);
imagefill($srcImage, 0, 0, $alpha);
imagesavealpha($srcImage, true);

$loadedImage=imagecreatefrompng($inputFilePath);
imagepalettetotruecolor($loadedImage);
imagesavealpha($loadedImage, true);

imagecopyresampled($srcImage, $loadedImage, 0, 0, 0, 0, $destWidth, $destHeight, $srcWidth, $srcHeight);

if(isImageType('stylize')) {
    $srcWidth = $destWidth;
    $srcHeight = $destHeight;
}

$waifuScaleFactor = 0.75;
$finalScaleFactor = 1.5;

if(isImageType('character')) {
    if($dollar == true) { // Dollar means it's just a single character
        $tilesX = 4;
        $tilesY = 1;
    } else {
        $tilesX = 12;
        $tilesY = 8;
    }
    $padding = true;
    $borderSize = 2;
}

if(isImageType('icons')) {
    $tilesX = $srcWidth / 24;
    $tilesY = $srcHeight / 24;
    $padding = true;
    $borderSize = 4;
    $waifuScaleFactor = 1 / 1.5;
    $finalScaleFactor = 32 / 24;
}

if(isImageType('faceset')) {
    $tilesX = 4;
    $tilesY = 2;
    $padding = true;
    $borderSize = 8;
}

if(isImageType('tileset')) {
    $tilesX = $srcWidth / 32;
    $tilesY = $srcHeight / 32;
    $padding = false;
}

$tileWidth = $srcWidth / $tilesX;
$tileHeight = $srcHeight / $tilesY;

if($padding == true) {
    $destImage = imagecreatetruecolor($srcWidth + (($tilesX + 1) * $borderSize), $srcHeight + (($tilesY + 1) * $borderSize));

    // Transparent!
    $alpha = imagecolorallocatealpha($destImage, 0, 0, 0, 127);
    imagefill($destImage, 0, 0, $alpha);
    imagesavealpha($destImage, true);

    for($w = 0; $w < ($srcWidth / $tileWidth); $w++) {
        for($h = 0; $h < ($srcHeight / $tileHeight); $h++) {
            $x = $w * $tileWidth;
            $y = $h * $tileHeight;

            imagecopy($destImage, $srcImage,
                $x + ($w * $borderSize) + $borderSize,
                $y + ($h * $borderSize) + $borderSize, $x, $y, $tileWidth, $tileHeight);
        }
    }
} else {
    $destImage = imagecreatetruecolor($srcWidth, $srcHeight);

    $alpha = imagecolorallocatealpha($destImage, 0, 0, 0, 127);
    imagefill($destImage, 0, 0, $alpha);
    imagesavealpha($destImage, true);
    imagecopy($destImage, $srcImage, 0, 0, 0, 0, $srcWidth, $srcHeight);
}

imagepng($destImage, $inputFilePath . '.padded');

$data = array(
            'url' => 'http://www.kickbackgames.com/public/mvfu/' . $inputFileName . '.padded',
            'noise' => 0,
            'scale' => 2);
$opts = array('http' =>
    array(
        'method' => 'POST',
        'header' => 'Content-Type: application/x-www-form-urlencoded',
        'content' => http_build_query($data)
    )
);

// Get Waifu'd PNG
$result = @file_get_contents('http://waifu2x.udp.jp/api', false, stream_context_create($opts));
if($result === false) {
    error_log('waifu error');
    $error = 'Error invoking waifu';
    removeDirectory($tempDir);
    return $error;
}

if(file_exists($inputFilePath . '.padded')) {
    unlink($inputFilePath . '.padded');
}

$waifuImage = imagecreatefromstring($result);
imagealphablending($waifuImage, false);
imagesavealpha($waifuImage, true);
$waifuWidth = imagesx($waifuImage);
$waifuHeight = imagesy($waifuImage);

ob_start();
imagepng($waifuImage);
$waifuString = ob_get_clean();

// Reduce the image with filtering
$waifu = new Imagick();
$waifu->readImageBlob($waifuString);
$waifu->resizeImage($waifuWidth * $waifuScaleFactor, $waifuHeight * $waifuScaleFactor, Imagick::FILTER_LANCZOS, 1);
$waifu->setImageFormat('png32');
$destString = $waifu->getImageBlob();

$waifuImage = imagecreatefromstring($destString);
imagesavealpha($waifuImage, true);
$waifuWidth = $waifuWidth * $waifuScaleFactor;
$waifuHeight = $waifuHeight * $waifuScaleFactor;

$destWidth = $srcWidth * $finalScaleFactor;
$destHeight = $srcHeight * $finalScaleFactor;
$tileWidth = $destWidth / $tilesX;
$tileHeight = $destHeight / $tilesY;
$borderSize = $borderSize * $finalScaleFactor;

$destImage = imagecreatetruecolor($destWidth, $destHeight);
$alpha = imagecolorallocatealpha($destImage, 0, 0, 0, 127);
imagefill($destImage, 0, 0, $alpha);
imagealphablending($destImage, false);
imagesavealpha($destImage, true);

if($padding == true) {
    for($w = 0; $w < ($destWidth / $tileWidth); $w++) {
        for($h = 0; $h < ($destHeight / $tileHeight); $h++) {
            $x = $w * $tileWidth;
            $y = $h * $tileHeight;

            imagecopy($destImage, $waifuImage,
                $x, $y, $x + ($w * $borderSize) + $borderSize,
                $y + ($h * $borderSize) + $borderSize, $tileWidth, $tileHeight);
        }
    }
} else {
    imagecopy($destImage, $waifuImage, 0, 0, 0, 0, $waifuWidth, $waifuHeight);
}

// Generate the alpha channel
ob_start();
imagepng($srcImage);
$srcString = ob_get_clean();

if(strpos($imageType, 'tilesets') !== false) { // For tilesets, use a special method
    $nn = new Imagick(); // Nearest-neighbor scaled image
    $nn->readImageBlob($srcString);
    $nn->resizeImage($srcWidth * $finalScaleFactor, $srcHeight * $finalScaleFactor, Imagick::FILTER_POINT, 1);
    $nn->setImageFormat('png32');
    $nnString = $nn->getImageBlob();

    $nnImage = imagecreatefromstring($nnString);
    imagesavealpha($nnImage, true);
    imagealphablending($nnImage, false);

    $rs = new Imagick(); // Bicubic (Lanczos) scaled image
    $rs->readImageBlob($srcString);
    $rs->resizeImage($srcWidth * $finalScaleFactor, $srcHeight * $finalScaleFactor, Imagick::FILTER_LANCZOS, 1);
    $rs->setImageFormat('png32');
    $rs->setImageMatte(true);
    $resampledString = $rs->getImageBlob();

    $resampledImage = imagecreatefromstring($resampledString);
    imagesavealpha($resampledImage, true);
    imagealphablending($resampledImage, false);

    $maskFile = '';
    switch($imageType) {
        case 'tilesets_a1a2_vx': $maskFile = 'assets/512_384_auto_mask.png'; break;
        case 'tilesets_a3_vx': $maskFile = 'assets/768_384_auto_mask.png'; break;
        case 'tilesets_a4_vx': $maskFile = 'assets/768_720_auto_mask.png'; break;
        case 'tilesets_a5_vx': $maskFile = 'assets/384_768_free_mask.png'; break;
        case 'tilesets_bcde_vx': $maskFile = 'assets/512_512_free_mask.png'; break;
        case 'tilesets_a1a2_mv_stylize': $maskFile = 'assets/512_384_auto_mask.png'; break;
        case 'tilesets_a3_mv_stylize': $maskFile = 'assets/768_384_auto_mask.png'; break;
        case 'tilesets_a4_mv_stylize': $maskFile = 'assets/768_720_auto_mask.png'; break;
        case 'tilesets_a5_mv_stylize': $maskFile = 'assets/384_768_free_mask.png'; break;
        case 'tilesets_bcde_mv_stylize': $maskFile = 'assets/512_512_free_mask.png'; break;
    }

    if(empty($maskFile)) {
        $error = 'Tile mask not found';
        removeDirectory($tempDir);
        return $error;
    }

    $nn->setImageMatte(true);

    $mask = new Imagick($maskFile); // Load mask
    $mask->setImageFormat('png32');
    $mask->setImageMatte(true);
    $nn->compositeImage($mask, Imagick::COMPOSITE_DSTIN, 0, 0);

    ob_start();
    imagepng($destImage);
    $destString = ob_get_clean();

    $im = new Imagick();
    $im->readImageBlob($destString);
    $im->setImageFormat('png32');
    $im->compositeImage($nn, Imagick::COMPOSITE_DEFAULT, 0, 0);
    $destString = $im->getImageBlob();

    $destImage = imagecreatefromstring($destString);
    imagesavealpha($destImage, true);
    imagealphablending($destImage, false);

    $maxAlpha = 127;
    $minAlpha = 5;
    $edgeDistanceX = 0;
    $edgeDistanceY = 0;

    // Calculate alpha channel...
    for($x = 0; $x < $destWidth; $x++) {
        for($y = 0; $y < $destHeight; $y++) {
            $a = $x % 48;
            $b = $y % 48;
            if($a > 24) {
                $edgeDistanceX = 47 - $a;
            } else $edgeDistanceX = $a;
            if($b > 24) {
                $edgeDistanceY = 47 - $b;
            } else $edgeDistanceY = $b;

            $rgb = imagecolorat($destImage, $x, $y);
            $dAlpha = $maxAlpha - (($rgb >> 24) & 0xFF);
            $r = ($rgb >> 16) & 0xFF;
            $g = ($rgb >> 8) & 0xFF;
            $b = $rgb & 0xFF;

            // Get alpha value of the nearest neighbour image
            $rAlpha = $maxAlpha - ((imagecolorat($nnImage, $x, $y) >> 24) & 0xFF);

            // Fix edges with some fun opacity logic
            if($edgeDistanceX < 2 || $edgeDistanceY < 2) {
                if($dAlpha == $maxAlpha) { // If WAIFU = SOLID
                    $alpha = $dAlpha; // Always use WAIFU
                } else if($dAlpha > $minAlpha) { // If WAIFU = NOT SOLID
                    // Semi-transparent border areas always use NN colours
                    $rgb = imagecolorat($resampledImage, $x, $y);
                    $r = ($rgb >> 16) & 0xFF;
                    $g = ($rgb >> 8) & 0xFF;
                    $b = $rgb & 0xFF;

                    if($rAlpha == $maxAlpha) { // And NN = SOLID
                        $alpha = $rAlpha; // Use NN
                    } else if($rAlpha < $minAlpha) { // If WAIFU = NOT SOLID and NN = TRANSPARENT, set transparent
                        $alpha = 0; // set TRANSPARENT
                    } else $alpha = $maxAlpha - (($rgb >> 24) & 0xFF); // Otherwise use original NN alpha
                } else { // If WAIFU = TRANSPARENT
                    if($rAlpha == $maxAlpha) { // And NN = SOLID
                        $alpha = $rAlpha; // Use NN
                    } else $alpha = 0; // Otherwise set TRANSPARENT
                }
            } else $alpha = $maxAlpha - (imagecolorat($resampledImage, $x, $y) >> 24) & 0xFF;

            $color = imagecolorallocatealpha($destImage, $r, $g, $b, $maxAlpha - $alpha);
            imagesetpixel($destImage, $x, $y, $color);
        }
    }
} else {
    $im = new Imagick();
    $im->readImageBlob($srcString);
    $im->resizeImage($srcWidth * $finalScaleFactor, $srcHeight * $finalScaleFactor, Imagick::FILTER_LANCZOS, 1);
    $im->setImageFormat('png32');
    $resampledString = $im->getImageBlob();

    $resampledImage = imagecreatefromstring($resampledString);
    imagesavealpha($resampledImage, true);
    imagealphablending($resampledImage, false);

    // Copy alpha channel from the standard resampled image
    for($x = 0; $x < $destWidth; $x++) {
        for($y = 0; $y < $destHeight; $y++) {
            $alpha = (imagecolorat($resampledImage, $x, $y) >> 24) & 0xFF;
            $rgb = imagecolorat($destImage, $x, $y);
            $r = ($rgb >> 16) & 0xFF;
            $g = ($rgb >> 8) & 0xFF;
            $b = $rgb & 0xFF;
            $color = imagecolorallocatealpha($destImage, $r, $g, $b, $alpha);
            imagesetpixel($destImage, $x, $y, $color);
        }
    }
}

$endTime = time();

// Save the image and send the session ID so we can get the image again
imagepng($destImage, $outputFilePath);
$output = json_encode(array('status' => 'success', 'message' => $sessionId));
echo $output;

// Clear temp files
removeDirectory($tempDir);

/**  ------ OUTPUT ------  **/
//header("Content-Type: image/png");
//$output = file_get_contents($outputFilePath);

Finally, the mask assets as referenced in the code above can be grabbed from e.g http://kickbackgames.com/mvfu/assets/512_512_free_mask.png.

Thanks for using it everyone, we had over 70,000 successful conversions, which is mind-boggling considering I originally made the tool to just do my own personal conversions (so it was expected that it'd be used maybe 20-100 times total). I sincerely hope someone has the time to turn the above code into something you can all use.

Over and out :)
 

AggroMiau

Villager
Member
Joined
Dec 31, 2015
Messages
16
Reaction score
6
First Language
German
Primarily Uses
N/A
Ohh soooo sad it's down I took a break in RPG-Maker and just came back to see the site is down *sniff*. :osad:
I loved to your tool so much. Thanks a lot for sharing it with us. It was a wondefull time I will never forget.
:kaoluv:
 

raffle

Veteran
Veteran
Joined
Oct 25, 2020
Messages
45
Reaction score
10
First Language
English
Primarily Uses
RMMV
What a shame, this was exactly what I was looking for! ;_; Thanks for your work and I hope someone will pick it up from here to keep it alive
 

seyfer110

Veteran
Veteran
Joined
Jan 9, 2014
Messages
39
Reaction score
2
Primarily Uses
Uhm, is there a way to batch convert images with waifu2x?

EDIT: never mind, I got it! ^.^d
 
Last edited:

Latest Threads

Latest Posts

Latest Profile Posts

I just found out that MZ regional price on Steam in my place is much cheaper than MV.
Our water started to work again, now i don't have to fill up our toilet with water again.
Finally able to return to work after months of lockdown. My poor feet are hurting :( I need to get used to working again haha
My birthday was yesterday and tomorrow marks the day my grandpa died around 5-6 years ago. I think about him a lot, and surprisingly had grown to become interested in programming- something he'd done for a living among other things. Funny how life works. I'm just glad I was able to enjoy yesterday with my immediate family. Anyway- guess who's 9 + 10?

Forum statistics

Threads
108,876
Messages
1,040,218
Members
141,311
Latest member
Ciifer
Top