Created
February 18, 2025 08:20
-
-
Save ipcjs/654843bc766a35d2b10a87deebe4554a to your computer and use it in GitHub Desktop.
Decomposes a merged image, extracting the foreground based on a background image and a specified source RGB color.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
# -*- coding: utf-8 -*- | |
import os | |
import sys | |
from typing import Dict, List, Tuple | |
# pip install Pillow | |
from PIL import Image | |
def logd(*args, **kw): | |
# print(*args, **kw) | |
pass | |
def print_info(im: Image): | |
print(im.format, im.mode, im.size) | |
return im | |
def image_to_string(image: Image): | |
return '%s(%s/%s/%s)' % (image.filename if image.format is not None else '??', image.mode, image.format, image.size) if image is not None else 'None' | |
# ico_bg.crop((0, 0, 100, 100)).show() | |
# ico_bg.paste(ico_fg, (0, 150)) | |
# ico_bg.show() | |
# ico_bg.point(lambda i: i * 2) | |
# Image.alpha_composite(ico_bg, ico_fg).show() | |
# Image.blend(ico_bg, ico_fg, 0.5).show() | |
def color_float_to_int(rgba: Tuple[float, ...]) -> Tuple[int, ...]: | |
return tuple(map(lambda x: round(x * 255), rgba)) | |
def color_int_to_float(rgba: Tuple[int, ...]) -> Tuple[float, ...]: | |
return map(lambda x: x/255, rgba) | |
def composePixel(srcRGBA, dstRGBA): | |
""" | |
混合 | |
:param srcRGBA: | |
:param dstRGBA: | |
:return: | |
""" | |
# print(srcRGBA, dstRGBA) | |
srcR, srcG, srcB, srcA = color_int_to_float(srcRGBA) | |
dstR, dstG, dstB, dstA = color_int_to_float(dstRGBA) | |
outA = srcA + dstA * (1 - srcA) | |
def rgb(src, dst): return (src * srcA + dst * dstA * (1 - srcA)) / outA | |
outRGBA = (rgb(srcR, dstR), rgb(srcG, dstG), rgb(srcB, dstB), outA) | |
return color_float_to_int(outRGBA) | |
def decomposePixelInvalid(outRGBA, dstRGBA): | |
""" | |
无效, 无法反混合 | |
:param outRGBA: | |
:param dstRGBA: | |
:return: | |
""" | |
print(outRGBA, dstRGBA) | |
dstR, dstG, dstB, dstA = color_int_to_float(dstRGBA) | |
outR, outG, outB, outA = color_int_to_float(outRGBA) | |
srcA = (outA - dstA) / (1 - dstA) | |
def rbg(out, dst): return (out * outA - dst * dstA * (1 - srcA)) / srcA | |
srcRGBA = (rbg(outR, dstR), rbg(outG, dstG), rbg(outB, dstB), srcA) | |
return color_float_to_int(srcRGBA) | |
def decomposePixel(outRGBA, dstRGBA, srcRGB): | |
""" | |
预设src的RGB值, 计算透明度的变化, 效果勉强可以 | |
:param outRGBA: | |
:param dstRGBA: | |
:return: | |
""" | |
if dstRGBA == (0, 0, 0, 0): | |
# 无背景时, 合并结果就是前景 | |
return outRGBA | |
dstR, dstG, dstB, dstA = color_int_to_float(dstRGBA) | |
outR, outG, outB, outA = color_int_to_float(outRGBA) | |
srcR, srcG, srcB = srcRGB | |
def safeDiv(a, b): return a / b if b != 0 else -1 | |
if dstRGBA == outRGBA: | |
srcA = 0 | |
else: | |
def alpha(src, dst, out): return safeDiv((out * outA - dst * dstA), (src - dst * dstA)) | |
srcRA = alpha(srcR, dstR, outR) | |
srcGA = alpha(srcG, dstG, outG) | |
srcBA = alpha(srcB, dstB, outB) | |
logd(outRGBA, dstRGBA, srcRA, srcGA, srcBA) | |
# srcA = srcRA if srcRA != -1 else srcGA if srcGA != -1 else srcBA if srcBA != -1 else 0 | |
srcA = max(srcRA, srcGA, srcBA) | |
srcRGBA = (*srcRGB, srcA) | |
# print(outRGBA, dstRGBA, '=>', srcRGBA) | |
return color_float_to_int(srcRGBA) | |
def composeImage(ico_fg, ico_bg): | |
ico_fg = ico_fg.convert('RGBA') | |
ico_bg = ico_bg.convert('RGBA') | |
ico_merge = Image.new('RGBA', ico_bg.size) | |
for x in range(0, ico_merge.width): | |
for y in range(0, ico_merge.height): | |
xy = (x, y) | |
ico_merge.putpixel(xy, composePixel(ico_fg.getpixel(xy), ico_bg.getpixel(xy))) | |
# ico_merge.show() | |
return ico_merge | |
def decomposeImage(ico_merge, ico_bg, srcRGB=(1, 1, 1)): | |
""" | |
:param srcRGB: 前景颜色的RGB值 | |
""" | |
logd('decomposeImage(%s, %s, srcRGB=%s)' % (image_to_string(ico_merge), image_to_string(ico_bg), srcRGB)) | |
ico_merge = ico_merge.convert('RGBA') | |
ico_bg = ico_bg.convert('RGBA') | |
ico_src = Image.new('RGBA', ico_merge.size, color=(*srcRGB, 0)) | |
for x in range(0, ico_src.width): | |
for y in range(0, ico_src.height): | |
xy = (x, y) | |
ico_src.putpixel(xy, decomposePixel(ico_merge.getpixel(xy), ico_bg.getpixel(xy), srcRGB)) | |
# ico_src.save(ICO_OUT) | |
return ico_src | |
ICO_OUT = 'temp/ico_out.png' | |
def open_ico_out(ico_path=ICO_OUT): | |
if sys.platform == 'win32': | |
os.system(f"start {ico_path}") | |
else: | |
os.system(f"open {ico_path}") | |
def test(): | |
ICO_MERGE = 'temp/ico_merge.png' | |
ICO_FG = 'temp/ico_fg.png' | |
ICO_BG = 'temp/ico_bg.png' | |
ico_merge = print_info(Image.open(ICO_MERGE)) | |
ico_fg = print_info(Image.open(ICO_FG)) | |
ico_bg = print_info(Image.open(ICO_BG)) | |
# ico_out = composeImage(ico_fg, ico_bg) | |
ico_out = decomposeImage(ico_merge, ico_bg, (1, 1, 1)) | |
ico_out.save(ICO_OUT) | |
open_ico_out() | |
def test2(): | |
ico_merge = Image.open(r"D:\JiangSong\Documents\Tencent Files\\1025695453\FileRecv\image(1)\image\live_icn_stop_video_landscape_sel_2x.png") | |
ico_bg = Image.open(r"D:\JiangSong\Documents\Tencent Files\\1025695453\FileRecv\image(1)\image\live_icn_speed_video_landscape_bg_sel_2x.png") | |
decomposeImage(ico_merge, ico_bg).save(ICO_OUT) | |
open_ico_out() | |
def processSelpicIcon(): | |
ico_merge = Image.open(r"C:\GitHub\@selpic\P660_printer\Printer\\app\src\main\\res\mipmap-xxxhdpi\ic_launcher.png") | |
ico_bg = Image.open(r"C:\GitHub\@selpic\P660_printer\Printer\\app\src\main\\res\mipmap-xxxhdpi\ic_launcher_bg.png") | |
decomposeImage(ico_merge, ico_bg).save(ICO_OUT) | |
open_ico_out() | |
def processIoStatusIcon(): | |
decomposeImage(Image.open('temp/ico_io_door_off.png'), Image.open('temp/ico_io_door_off_bg.png'), (0, 0, 0)).save('temp/ico_io_door_off_fg.png') | |
decomposeImage(Image.open('temp/ico_io_door_on.png'), Image.open('temp/ico_io_door_on_bg.png'), (0, 0, 0)).save('temp/ico_io_door_on_fg.png') | |
def processTgBusIcon(): | |
ico_merge = Image.open(r'C:/GitHub/@tg/flutter_distar_ex17/ios/Runner/Assets.xcassets/AppIcon.appiconset/[email protected]') | |
ico_bg = Image.new('RGBA', ico_merge.size, color='#00a35a') | |
decomposeImage(ico_merge, ico_bg).save(ICO_OUT) | |
open_ico_out() | |
def processTgIcon(): | |
ico_merge = Image.open(r'../../../../@tg/Tracker-iOS/SNProject/SNProject/AssetsTG.xcassets/AppIcon.appiconset/ICON_1024.png') | |
ico_bg = Image.new('RGBA', ico_merge.size, color='#db4b17') | |
decomposeImage(ico_merge, ico_bg).save(ICO_OUT) | |
open_ico_out() | |
def processBlaupunktIcon(): | |
ico_merge = Image.open(r'../../../../@tg/Tracker-Android/app/src/blaupunkt/res/mipmap-xxhdpi/ic_launcher_foreground.png') | |
ico_bg = Image.new('RGBA', ico_merge.size, color='#003867') | |
decomposeImage(ico_merge, ico_bg).save(ICO_OUT) | |
open_ico_out() | |
def processStoneIcon(): | |
""" | |
处理萤石的icon(;¬_¬) | |
""" | |
STONE_ICON_DIR = 'temp/stoneIcon' | |
ICO_BG_DEFAULT_FILE = os.path.join(STONE_ICON_DIR, 'ico_bg.png') | |
ico_bg_default = Image.open(ICO_BG_DEFAULT_FILE) if os.path.exists(ICO_BG_DEFAULT_FILE) else None | |
files = (os.path.join(STONE_ICON_DIR, name) for name in os.listdir(STONE_ICON_DIR) if not os.path.splitext(name)[0].endswith('_bg')) | |
for f in files: | |
print('process: %s' % f) | |
ico_bg_file = '%s_bg%s' % os.path.splitext(f) | |
ico_fg_file = '%s_fg%s' % os.path.splitext(f) | |
ico_bg = Image.open(ico_bg_file) if os.path.exists(ico_bg_file) else ico_bg_default | |
ico = Image.open(f) | |
decomposeImage(ico, ico_bg, srcRGB=(1, 1, 1)).save(ico_fg_file) | |
if __name__ == '__main__': | |
# test() | |
# test2() | |
# processStoneIcon() | |
# processSelpicIcon() | |
# processTgIcon() | |
# processBlaupunktIcon() | |
processIoStatusIcon() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment