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,378
Reaction score
1,303
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
234
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
37
Reaction score
6
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
 

Users Who Are Viewing This Thread (Users: 0, Guests: 2)

Latest Threads

Latest Posts

Latest Profile Posts

While we prepare the official trailer, enjoy this kind-of-second teaser! ^^
-Ele
New Episodes of RPG Shenanigans Uploaded to Youtube!

Episode 5 - Surprise Party!
Youtube Link:
Episode 6 - Killer Gin
Youtube Link:
Episode 7 - Gaia's Melody: Echoed Melodies
(Coming soon!)

Episode 8 - Clarent Saga: Tactics
(Coming soon!)

Episode 9 - Star Shift
(Coming soon!)
When the Map Generator throws in the assets in the most dumbest way possible - your path is blocked :D

I went to sleep at 3 am because of my anxiety. Set up my alarm for 7 am so that I could have sasagues for breakfast and do morning routine before lessons starts at 8 am. I knew I wouldn't be able to sleep even after my lessons finished because I have to visit my grandparents today I was sad bc I was really tired. Thats when I realised. My lesson starts at 9 am. I could get one extra hour of sleep if I didnt forget it

Forum statistics

Threads
107,561
Messages
1,030,563
Members
139,672
Latest member
WDRS
Top