Pako96 Posted April 17, 2023 Posted April 17, 2023 Hi everybody. In my free time I decided to do some little research on the format in which trainer photo/icon are saved in S/V. After some trials and errors, I descovered that each pixel is represented by a sequence of 8 bytes, of which the first 2 represent colors in BGR 565 encoding and the third is some sort of Alpha channel transparency. I'm currently not aware of the purpose of the other bytes, since editing them didn't bring any noticeable change. Then, thanks to this recent commit , I was able to get the correct size and aspect ratio for the images, and I assembled a little python script that I'm attaching to this post. It is pretty simple to use, just open the save block editor in PkHex, save the image block you desire to see, check the corrisponding values for width - lenght - size (take for reference the previous link until PkHex doesn't get an official update), finally run the script. For example, if we want to save the current profile picture: export the block at 0x14C5A101 as 'picture.bin' check the width at 0xFEAA87DA, e.g. 1440 check the height at 0x5361CEB5, e.g. 832 check size at 0x1E505002, e.g. 599040 let's save the output as 'current.png' The code in the terminal, after changing directory to the one in which we find both picture.bin and imgdec.py, will be: python imgdec.py picture.bin 1440 832 599040 -o current.png In the future I'll try to understand better the other bytes of the encoding and maybe build a little editor/injector for custom images. Of course you are free to make any change you want, and if you want to share new ideas let me know in the replies! Alongside the script I also attach some example output images from my saves. imgdec.py 3
foohyfooh Posted April 22, 2023 Posted April 22, 2023 (edited) Ported this to PKHeX Plugin. Download here: https://projectpokemon.org/home/files/file/4961-pkhex-plugin-pile/ Source here: https://github.com/foohyfooh/PKHeXPluginPile Edited April 22, 2023 by foohyfooh 1
Pako96 Posted April 22, 2023 Author Posted April 22, 2023 (edited) UPDATE I still haven't figured out what the other bytes do, but I can confirm that replacing them with 00 padding doesn't create any new problems. So, I edited a bit the script and now you can create your custom .bin for trainer photo/icon with any image you like. It's still unpolished, and it's hardcoded to picture sizes i deem 'safe' (for some reason the actual images may be smaller than the values registered in the dims blocks). Of course the .bin produced must override the corrisponding old block via PkHex, and the other blocks (width, height, size) must be overwritten with values i put in the comments inside the script. Still needs work, but it's getting fun. Of course, you are still free to make any change you wish and rework the script as you please (like how foohyfooh did and may still do, if he wishes to incorporate the new changes for an eventual injector) 4 hours ago, foohyfooh said: Ported this to PKHeX Plugin. Download here: https://projectpokemon.org/home/files/file/4961-pkhex-plugin-pile/ Source here: https://github.com/foohyfooh/PKHeXPluginPile I attach an irl photo I took as proof, putting the trainer from PBR as background imgdec2.py Edited April 22, 2023 by Pako96 1
Kaphotics Posted April 22, 2023 Posted April 22, 2023 Obligatory reminder/warning about injecting images being bannable, as player photos are shared for PvP battles / etc. It is recommended to never inject images that end up being transmitted from your console, because it's a clear indicator of a hacked console. 1 1
Pako96 Posted April 24, 2023 Author Posted April 24, 2023 OTHER UPDATE While foohyfooh updates his plugin with the injector, I did some additional research on the images and found out that if you strip each 8 bytes sequence in 4 subsequences of 2 bytes and put them together accordingly you get 4 different masks. Bytes 0-1 of each pixel give the low res image in light mode, bytes 2-3 give low res dark mode, 4-5 and 6-7 give a sort of segmentation of the previous interpretations. I am conducting experiments on how to blend them together. If somebody is interested and skilled in image manipulation, don't hesitate to reply or cantact me. 2
foohyfooh Posted April 24, 2023 Posted April 24, 2023 (edited) Screenshot from game Light and Dark Images Light and Dark Masks I haven't gotten it working in the plugin but using GIMP and applying dark mask as a layer mask (darker equals more transparency), I got the resulting image where you can see the blue dot from the machine is showing up which is evident in the game screenshot of what the profile picture should be. But I still haven't figure out how the light mask is used. Joined Image Edited April 24, 2023 by foohyfooh 2
Pako96 Posted April 26, 2023 Author Posted April 26, 2023 Appearently computing the mean between the first two images (for each pixel of the 2 images: (pxIM1 + pxIM2) // 2) gives good results. Still figuring out what the last 2 images do. Also, given the values in the resolution blocks, I started to think that it could be a custom algorithm for subsampling. News will follow. 1
Pako96 Posted April 27, 2023 Author Posted April 27, 2023 Other source material I truncated the photo block to see how the game directly interprets the various components. First one is the normal photo Second one is photo without masks (just bytes 0-1-2-3), and looks pixelated/low res Third one is photo with only masks (bytes 4-5-6-7), could be a XOR between the two masks 1
Pako96 Posted April 27, 2023 Author Posted April 27, 2023 (edited) Single blocks display First two bytes constitute the basic low res image Other two bytes for the dark image aren't shown with the first two bytes black, so it's possible that a logic AND is performed between the first two groups of bytes The two masks perfectly overlap in the image I put in the previous reply, furthermore it's very likely they need to be read as greyscale since they are both black and white. Also they are probably there as alpha channel balancing to make the original image look smoother EDIT: I can nearly confirm that light image and dark image have to be alpha blended to make the final image. With this logic is explainable why performing statisthical mean pixel by pixel works, since it represents an alpha blending with alpha fixed at 0.5. Now it needs to be understood how to apply the composite mask for blending. My guess was performing arithmetically the alpha blending pixel by pixel using as alpha the luminance intensity of the corrisponding pixel in the mask. Another way could be computing a fixed alpha as number of white pixels / number ot total pixels in the mask. Edited April 27, 2023 by Pako96 Further Development 1
Pako96 Posted April 27, 2023 Author Posted April 27, 2023 Probably last update I kept experimenting and I made some acceptable results, even if nothing is to be taken for sure since the true algorithm will never be released to the pubilc. The original image: The new experiment: it was obatained as it follows: save the light image and the dark image, save also the light mask and the dark mask and make a third mask with bitwise pixel by pixel OR of the two original masks (the mask have to be read as grayscale images, normalizing the value of each pixel between 0 - 255). Then for each pixel of the 2 initial images we do alpha blending taking as alpha the corrisponding pixel value of the or mask. So for the 3 channels we'll have the following: #assuming r,g,b as the three channels and the two images expressed as a list of tuples (r,g,b), where each tuple is a pixel, the lists are named lightIM and darkIM; in addition the mask is a list of values between 0-255 named mask. The final image is an empty list named finalIM for px in range(len(lightIM)): alpha = mask[px] newR = int(lightIM[px][0] * (1 - alpha) + darkIM[px][0] * alpha) newG = int(lightIM[px][1] * (1 - alpha) + darkIM[px][1] * alpha) newB = int(lightIM[px][2] * (1 - alpha) + darkIM[px][2] * alpha) finalIM.append((newR,newG,newB)) image = Image.new("RGB", (int(width/4), int(height/4))) image.putdata(finalIM) image.save("final.png") Obtaining these: The old algebrical pixel by pixel average: As you can see they look pretty similar, with the new experiment being a little smoother, but maybe the overworld light isn't completely correct. Anyway I leave this here in order to be reasearched further by whoever wants to give a try or implement it in any form. I'll stop for some time on this topic, but I'll still keep an eye when I'd be able to. 1
foohyfooh Posted April 28, 2023 Posted April 28, 2023 Could you upload the script you did to get the recent results?
Pako96 Posted April 28, 2023 Author Posted April 28, 2023 Here you go. I also made a pull request on your GitHub with a variation to improve light imgdec(1).py
PizzaTimeJoshua Posted September 8, 2023 Posted September 8, 2023 Turns out the image was compressed using DXT1 compression. Injecting images should be great now. Anyways I saved you all the trouble with image stuff and made a Python script and executable with tutorial on how to inject/extract images. Here's the link. I worked hard on this so I hope someone finds this useful: https://github.com/PizzaTimeJoshua/SV-Image-Injector 1
PizzaTimeJoshua Posted September 8, 2023 Posted September 8, 2023 Whoops, hard coded some sizes for the width and height that apparently change (for reasons unknown) e.g. *UInt32 KPictureProfileCurrentWidth was observed to be either 960 or 1440. Changing the values allows for a higher quality image to be rendered onto the Profile and Icon but also means you need to check/change the values every time you want to inject an image. Did testing on my old save file which had 960 as width but then when I tried it on a newer save file, it has 1440 as a width, which made the image come out incorrectly. I will remedy this issue (or try) as soon as I can. Damn these inconsistent sizes! 1
PizzaTimeJoshua Posted September 17, 2023 Posted September 17, 2023 Updated the code to be nicer to look and added the option to change the sizes if you find that they are different from your own save file. 2
Recommended Posts
Create an account or sign in to comment
You need to be a member in order to leave a comment
Create an account
Sign up for a new account in our community. It's easy!
Register a new accountSign in
Already have an account? Sign in here.
Sign In Now