-
Notifications
You must be signed in to change notification settings - Fork 0
/
cchedsDecode.py
222 lines (193 loc) · 7.88 KB
/
cchedsDecode.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
import numpy as np
import cv2
import base64 as b64
import xxhash as xxh
def render(arr: list[str]) -> None:
# arr is a list of strings, each string is a row of the image
# The numbers in the string are the colours of the pixels
# 0 = black (0, 0, 0), 1 = blue (0, 0, 255), 2 = green (0, 255, 0), 3 = cyan (0, 255, 255)...
# Show this using cv2
# Convert the strings to a list of tuples
number_to_tuple = lambda letter: tuple([255 if int(n) else 0 for n in bin(letter)[2:].zfill(3)])
arr = [[number_to_tuple(int(letter)) for letter in row] for row in arr]
arr = [[(i[2], i[1], i[0]) for i in row] for row in arr]
# Arr is a list of lists of tuples, each tuple is a pixel with an RGB value
# Convert this to a numpy array
arr = np.array(arr, dtype=np.uint8)
# Show the image
cv2.imshow("Image", arr)
cv2.waitKey(0)
cv2.destroyAllWindows()
def find_code() -> str:
# Load DecodeTest.jpg to a cv2 image
img = cv2.imread("DecodeTest.jpg")
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
# Resize to 1000 by 1000
img = cv2.resize(img, (600, 800))
img = cv2.GaussianBlur(img, (5, 5), 0)
lab= cv2.cvtColor(img, cv2.COLOR_BGR2LAB)
l_channel, a, b = cv2.split(lab)
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
cl = clahe.apply(l_channel)
limg = cv2.merge((cl,a,b))
enhanced_img = cv2.cvtColor(limg, cv2.COLOR_LAB2BGR)
edges = cv2.Canny(enhanced_img, 150, 200)
# img = cv2.adaptiveThreshold(img, 255, 1, 1, 11, 2)
(r,g,b) = cv2.split(img)
grey = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
cv2.imshow("img", img)
# cv2.imshow("r", r)
# cv2.imshow("g", g)
# cv2.imshow("b", b)
cv2.imshow("grey", grey)
cv2.imshow("Edges", edges)
cv2.waitKey(0)
cv2.destroyAllWindows()
def split(bytes, number):
l = []
s = ""
i = 0
m = 0
for b in bytes:
s += b
i += 1
m += 1
if i == number:
l.append(s)
s = ""
i = 0
if m == len(bytes):
if(len(s) < number):
s = s.ljust(number, "0")
l.append(s)
return l
def rotate(arr: list[str]) -> list[str]:
"""Rotate a 2D array 90 degrees clockwise"""
return ["".join(row) for row in zip(*arr[::-1])]
def flip_horizontal(arr: list[str]) -> list[str]:
"""Flip a 2D array horizontally"""
return [row[::-1] for row in arr]
def flip_vertical(arr: list[str]) -> list[str]:
"""Flip a 2D array vertically"""
return arr[::-1]
def flip_diagonal(arr: list[str]) -> list[str]:
"""Flip a 2D array diagonally"""
return [list(row) for row in zip(*arr)]
def corners_from_arr(arr: list[str]) -> list[list[str]]:
return [ # Top left, clockwise
[arr[0][0], arr[0][1], arr[1][1], arr[1][0]],
[arr[0][-2], arr[0][-1], arr[1][-1], arr[1][-2]],
[arr[-2][-2], arr[-2][-1], arr[-1][-1], arr[-1][-2]],
[arr[-2][0], arr[-2][1], arr[-1][1], arr[-1][0]]
]
def normalize_rotation(arr: list[str]) -> str:
corners = corners_from_arr(arr)
corner_sets = [set(c) for c in corners]
# Two diagonally opposite corners will contain all 8 numbers
if len(corner_sets[0] | corner_sets[2]) == 8:
# Code is valid
pass
elif len(corner_sets[1] | corner_sets[3]) == 8:
# Code is correct, but rotated 90 degrees.
arr = rotate(arr)
corners = corners[1:] + corners[0]
else:
raise Exception("No diagonally opposite corners contain all 8 colours - Invalid code")
# We now know the top left and bottom right corners are completely different, but they could be rotated
# The way to check a grid is correct is to check the bottom left corner
# The top 2 pixels match the top 2 pixels of the top left corner, and the bottom 2 match the bottom right corner
def find_valid_orientation(to_test: list[str]) -> list[str] | None:
"""Finds a valid rotation of the grid by rotating it, or None if no valid orientation exists"""
for _ in range(4):
current_corners = corners_from_arr(to_test)
expected = [current_corners[0][:2], current_corners[2][2:]]
check = [current_corners[3][:2], current_corners[3][2:]]
if expected == check:
return to_test
to_test = rotate(to_test)
return None
valid = find_valid_orientation(arr)
if not valid:
# The grid could be flipped horizontally, vertically or diagonally
methods = [flip_horizontal, flip_vertical, flip_diagonal]
for method in methods:
valid = find_valid_orientation(method(arr))
if valid:
break
if not valid:
raise Exception("No valid orientation found - Invalid code")
return valid
def generate_key(arr: list[str]) -> dict[str, str]:
"""Generate a key from the corners of a normalised grid"""
corners = corners_from_arr(arr)
return {
corners[0][0]: "0", corners[0][1]: "1", corners[0][2]: "2", corners[0][3]: "3",
corners[2][2]: "4", corners[2][3]: "5", corners[2][0]: "6", corners[2][1]: "7",
}
def check(decoded: str, arr: list[str]) -> bool:
"""Returns if the checksums and hashes match a normalised grid"""
# Hash using the correct algorithm
if arr[0][-2] == "0":
hasher = xxh.xxh64()
hasher.update(decoded)
hash_bytes = hasher.digest()
hash_bytes = b64.b64encode(hash_bytes)
hash_bytes = "".join([bin(n)[2:].zfill(8) for n in hash_bytes])
# Convert to 3bit strings (one for each pixel)
hash_bytes = split(hash_bytes, 3)
size = (len(arr[0]), len(arr))
# Add extra 0s if needed
if len(hash_bytes) < 2 * (size[0] + size[1]):
hash_bytes += ["0" * 3] * (size[0] + size[1] - len(hash_bytes))
# Find the limit of pixels on the right
cutoff = (2 * size[0]) - 2
for pixel_index in range(len(hash_bytes)):
if pixel_index < cutoff:
# The pixel should be on the right
x = size[0] + 2 + (pixel_index % 2)
y = 2 + (pixel_index // 2)
else:
# The pixel should be on the bottom
x = (pixel_index - cutoff) % size[0] + 2
y = size[1] + 2 + (pixel_index - cutoff) // size[0]
# Check if the pixel is out of bounds
if x >= size[0] or y >= size[1]:
break
# Instead of setting the pixel, we check if it matches the hash
if arr[y][x] != int(hash_bytes[pixel_index], 2):
return False
return True
cv2.QRCodeDetector
def decode(arr: list[str]) -> str:
arr = normalize_rotation(arr)
key = generate_key(arr)
# Replace each value in the grid with the corresponding value in the key
arr = ["".join([key[i] for i in row]) for row in arr]
# The data is the whole grid, except 2 pixels on each side
data = "".join([row[2:-2] for row in arr[2:-2]])
# Convert each digit to a 3 bit binary number
data = "".join([bin(int(i))[2:].zfill(3) for i in data])
# Convert this to a byte string
data = bytes([int(data[i:i+8], 2) for i in range(0, len(data), 8)])
# The original string was put through UTF-8 encoding, then base64. We need to reverse this
text = b64.b64decode(data).decode("utf8")
# We need to check that all the checksums and hashes match
is_valid = check(text, arr)
print("Valid:", is_valid)
return text
def main():
# text = "014102325201 322324221340 732544352615 533646210706 133307253215 723624112431 023104411236 133406115546 623063640075 017024661467 546535071454"
text = ""
if not text:
action = input("[T]ext or [F]ile: ")
if action.lower() == "f":
text = bytes(open(input("Filename: ")).read(), "utf-8")
else:
text = bytes(input("Text: "), "utf-8")
text = text.split()
print("Encoded:", "".join(text))
decoded = decode(text)
print("Decoded:", decoded)
if __name__ == "__main__":
find_code()
# main()