Created
April 12, 2024 15:45
-
-
Save josepablo-espinoza/aa028689af9e74e6540815dacc147dad to your computer and use it in GitHub Desktop.
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
from krita import * # type: ignore | |
from time import time | |
from PyQt5.QtCore import QByteArray | |
''' | |
Pseudo Pixel Perfect (PPP) post-process for Krita: | |
It removes double pixels (in an L-shaped configuration) | |
and then thins it using the Zhang-Suen thinning algorithm, | |
aiming to recreate the feel of a pixel-perfect stabilization brush. | |
The script checks for opaque pixels in a selection | |
and processes them based on their neighbors | |
to remove doubles, thin the line, and remove doubles again. | |
TODO: Locate the checkNeighbors algorithm. | |
''' | |
''' | |
Install: | |
1. Save or download this script. | |
2. Place it in any preferred folder. | |
3. Name it e.g. "pseudo-pixel-perfect.py". | |
4. On Krita use ten script plugin to assing this script to a shortcut. | |
How to use: | |
1. Draw | |
2. Create a selection covering the entire drawing or specific areas you want to clean. | |
3. Press the shortcut key you set earlier to activate the script. | |
Warning: | |
This process is DESTRUCTIVE. While you can usually undo it, there are some edge cases where unexpected behavior may occur: | |
- Large areas (over 1000 pixels) and thick lines. | |
- Lines with transparency. | |
- Very thick lines (over 10 pixels). | |
- Unusual selections may cause undo to behave unexpectedly. | |
This are no problems in the scope of pixel art, non the less exercise caution. | |
''' | |
NAME = 'PPP' | |
VERSION = "1.0" | |
TRANSPARENT_BYTE = b'\x00\x00\x00\x00' | |
DOC = Krita.instance().activeDocument() # type: ignore | |
CURRENT_LAYER = DOC.activeNode() | |
''' | |
TODO: Investigate this algorithm; something appears incorrect. | |
Note: Potential issues may arise if pixels have transparency or are not fully opaque. | |
''' | |
def checkNeighbors(r, l, u, d, ru, rd, lu, ld): | |
t = TRANSPARENT_BYTE | |
#□□□ | |
#□▣□ | |
#□□□ | |
if ((r == t and l == t and u == t and d == t and ru == t and rd == t and lu == t and ld == t)): | |
return True | |
#□□□ | |
#□▣■ | |
#□■□ | |
elif r != t and l == t and u == t and d != t and ru == t and rd == t and lu == t and ld == t: | |
return True | |
#□□□ | |
#■▣□ | |
#□■□ | |
elif l != t and r == t and u == t and d != t and ru == t and rd == t and lu == t and ld == t: | |
return True | |
#□■□ | |
#□▣■ | |
#□□□ | |
elif r != t and l == t and d == t and u != t and ru == t and rd == t and lu == t and ld == t: | |
return True | |
#□■□ | |
#■▣□ | |
#□□□ | |
elif l != t and r == t and d == t and u != t and ru == t and rd == t and lu == t and ld == t: | |
return True | |
#□■■ | |
#■▣□ | |
#■□□ | |
elif l != t and r == t and d == t and u != t and ru != t and rd == t and lu == t and ld != t: | |
return True | |
#■□□ | |
#■▣□ | |
#□■■ | |
elif l != t and r == t and u == t and d != t and rd != t and ru == t and ld == t and lu != t: | |
return True | |
#□□□ | |
#■▣□ | |
#□■■ | |
elif l != t and r == t and u == t and d != t and rd != t and ru == t and ld == t and lu == t: | |
return True | |
#□□■ | |
#□▣■ | |
#□■□ | |
elif r != t and l == t and u == t and d != t and ru != t and rd == t and lu == t: | |
return True | |
#□■■ | |
#■▣□ | |
#□□□ | |
elif l != t and r == t and d == t and u != t and rd == t and ru != t and ld == t and lu == t: | |
return True | |
#■■□ | |
#□▣■ | |
#□□□ | |
elif r != t and l == t and d == t and u != t and rd == t and ru == t and ld == t and lu != t: | |
return True | |
#□■□ | |
#□▣■ | |
#□□■ | |
elif r != t and l == t and d == t and u != t and rd != t and ru == t and ld == t and lu == t: | |
return True | |
#□■□ | |
#■▣□ | |
#■□□ | |
elif l != t and r == t and d == t and u != t and rd == t and ru == t and ld != t and lu == t: | |
return True | |
else: | |
return False | |
''' | |
Zhang-Suen thinning algorithm | |
https://web.archive.org/web/20160322113207/http://opencv-code.com/quick-tips/implementation-of-thinning-algorithm-in-opencv/ | |
''' | |
def thinningIteration(selection, iter): | |
sh = selection.height() | |
sw = selection.width() | |
sx = selection.x() | |
sy = selection.y() | |
selection_qbytearray = selection.pixelData(sx,sy,sw,sh) | |
t = TRANSPARENT_BYTE | |
byte_ = QByteArray(TRANSPARENT_BYTE) | |
''' | |
The selection_qbytearray is a flat array, | |
whereas the selection is a 2D array. | |
The loop variable is used to keep both aligned. | |
''' | |
loop = 0 | |
for j in range(sy, sy + sh): | |
for i in range(sx, sx + sw): | |
if selection_qbytearray.at(loop) == b'\xff' and CURRENT_LAYER.pixelData(i,j,1,1) != TRANSPARENT_BYTE: | |
# p2-9 follows a counter-clockwise order. | |
p6 = CURRENT_LAYER.pixelData(i+1, j+0, 1,1)#p6 | |
p2 = CURRENT_LAYER.pixelData(i-1, j+0, 1,1)#p2 | |
p8 = CURRENT_LAYER.pixelData(i+0, j-1, 1,1)#p8 | |
p4 = CURRENT_LAYER.pixelData(i+0, j+1, 1,1)#p4 | |
p7 = CURRENT_LAYER.pixelData(i+1, j-1, 1,1)#p7 | |
p5 = CURRENT_LAYER.pixelData(i+1, j+1, 1,1)#p5 | |
p9 = CURRENT_LAYER.pixelData(i-1, j-1, 1,1)#p9 | |
p3 = CURRENT_LAYER.pixelData(i-1, j+1, 1,1)#p3 | |
A = int(p2 ==t and p3 !=t) + int(p3 ==t and p4 !=t) + \ | |
int(p4 ==t and p5 !=t) + int(p5 ==t and p6 !=t) + \ | |
int(p6 ==t and p7 !=t) + int(p7 ==t and p8 !=t) + \ | |
int(p8 ==t and p9 !=t) + int(p9 ==t and p2 !=t) | |
B = int(p2 !=t) + int(p3 !=t) + int(p4 !=t) + int(p5 !=t) + int(p6 !=t) + int(p7 !=t) + int(p8 !=t) + int(p9 !=t); | |
m1 = (int(p2 !=t) * int(p4 !=t) * int(p6 !=t)) if iter == 0 else (int(p2 !=t) * int(p4 !=t) * int(p8 !=t)); | |
m2 = (int(p4 !=t) * int(p6 !=t) * int(p8 !=t)) if iter == 0 else (int(p2 !=t) * int(p6 !=t) * int(p8 !=t)); | |
if (A == 1 and (B >= 2 and B <= 6) and m1 == 0 and m2 == 0): | |
CURRENT_LAYER.setPixelData(byte_, i, j, 1, 1) | |
loop += 1 | |
''' | |
Zhang-Suen thinning algorithm typically employs | |
a do-while loop and checks against a mask. | |
For simplicity, two passes (iterations) | |
are usually sufficient. | |
Rerun if necessary. | |
''' | |
def thinning(selection): | |
thinningIteration(selection, 0) | |
thinningIteration(selection, 1) | |
''' | |
Loops through a selection. For each opaque pixel it checks its immediate neighbors. | |
Based on an algorithm, it identifies L-shaped arrangements | |
of pixels and breaks them by making the pixel transparent. | |
''' | |
def remove_doubles(selection): | |
sh = selection.height() | |
sw = selection.width() | |
sx = selection.x() | |
sy = selection.y() | |
selection_qbytearray = selection.pixelData(sx,sy,sw,sh) | |
byte_ = QByteArray(TRANSPARENT_BYTE)#transparent | |
''' | |
The selection_qbytearray is a flat array, | |
whereas the selection is a 2D array. | |
The loop variable is used to keep both aligned. | |
''' | |
loop = 0 | |
for j in range(sy, sy + sh): | |
for i in range(sx, sx + sw): | |
if selection_qbytearray.at(loop) == b'\xff' and CURRENT_LAYER.pixelData(i,j,1,1) != TRANSPARENT_BYTE: | |
r = CURRENT_LAYER.pixelData(i+1, j+0, 1,1)#p6 | |
l = CURRENT_LAYER.pixelData(i-1, j+0, 1,1)#p2 | |
u = CURRENT_LAYER.pixelData(i+0, j-1, 1,1)#p8 | |
d = CURRENT_LAYER.pixelData(i+0, j+1, 1,1)#p4 | |
ru = CURRENT_LAYER.pixelData(i+1, j-1, 1,1)#p7 | |
rd = CURRENT_LAYER.pixelData(i+1, j+1, 1,1)#p5 | |
lu = CURRENT_LAYER.pixelData(i-1, j-1, 1,1)#p9 | |
ld = CURRENT_LAYER.pixelData(i-1, j+1, 1,1)#p3 | |
if checkNeighbors(r, l, u, d, ru, rd, lu, ld): | |
CURRENT_LAYER.setPixelData(byte_, i, j, 1, 1) | |
loop += 1 | |
if not CURRENT_LAYER.locked() and CURRENT_LAYER.visible(): | |
selection = DOC.selection() | |
if selection: | |
start = time() | |
remove_doubles(selection) | |
thinning(selection) | |
remove_doubles(selection) | |
DOC.refreshProjection() | |
DOC.waitForDone() | |
end = time() | |
Krita.instance().activeWindow().activeView().showFloatingMessage(f"{NAME} completed! Duration: {round(end-start)} seconds.", Krita.instance().icon("dialog-ok"), 2000, 1) # type: ignore | |
print(f"{NAME} completed! Duration: {round(end-start)} seconds.") | |
else: | |
QMessageBox.information(QWidget(), "", "Please select an area first. Note that the wider the selection, the slower the process will be. \n This process is destructive, and in edge cases, a backup is advisable before use.") # type: ignore | |
print("No area has been selected.!") | |
else: | |
QMessageBox.information(QWidget(), "", "The layer is either locked or invisible!") # type: ignore | |
print("The layer is either locked or invisible!") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment