Skip to content

Instantly share code, notes, and snippets.

@ipcjs
Created February 18, 2025 08:20
Show Gist options
  • Save ipcjs/654843bc766a35d2b10a87deebe4554a to your computer and use it in GitHub Desktop.
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.
#!/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