питон
Кодирование требует NumPy , SciPy и Scikit-изображения .
Для декодирования требуется только PIL .
Это метод, основанный на суперпиксельной интерполяции. Для начала каждое изображение делится на 70 одинаковых по размеру областей одинакового цвета. Например, пейзажная картинка делится следующим образом:
Расположен центроид каждой области (до ближайшей растровой точки на сетке, содержащей не более 402 точек), а также ее средний цвет (из 216 цветовой палитры), и каждая из этих областей кодируется как число от 0 до 86832 , который может быть сохранен в виде 2,5 печатных символов ascii (фактически 2.497 , оставляя достаточно места для кодирования в битах оттенков серого).
Если вы внимательны, вы могли заметить, что 140 / 2,5 = 56 регионов, а не 70, как я говорил ранее. Однако обратите внимание, что каждая из этих областей является уникальным сопоставимым объектом, который может быть указан в любом порядке. Из-за этого мы можем использовать перестановку первых 56 областей для кодирования для остальных 14 , а также иметь несколько битов, оставшихся для сохранения соотношения сторон.
Более конкретно, каждая из дополнительных 14 областей преобразуется в число, и затем каждое из этих чисел объединяется вместе (умножая текущее значение на 86832 и добавляя следующее). Это (гигантское) число затем преобразуется в перестановку на 56 объектов.
Например:
from my_geom import *
# this can be any value from 0 to 56!, and it will map unambiguously to a permutation
num = 595132299344106583056657556772129922314933943196204990085065194829854239
perm = num2perm(num, 56)
print perm
print perm2num(perm)
будет выводить:
[0, 3, 33, 13, 26, 22, 54, 12, 53, 47, 8, 39, 19, 51, 18, 27, 1, 41, 50, 20, 5, 29, 46, 9, 42, 23, 4, 37, 21, 49, 2, 6, 55, 52, 36, 7, 43, 11, 30, 10, 34, 44, 24, 45, 32, 28, 17, 35, 15, 25, 48, 40, 38, 31, 16, 14]
595132299344106583056657556772129922314933943196204990085065194829854239
Полученная перестановка затем применяется к исходным 56 областям. Исходное число (и, следовательно, дополнительные 14 областей) также может быть извлечено путем преобразования перестановки 56 кодированных областей в ее числовое представление.
Когда --greyscale
опция используется с кодировщиком, вместо нее используются 94 области (разделенные 70 , 24 ), с 558 точками растра и 16 оттенками серого.
При декодировании каждая из этих областей рассматривается как трехмерный конус, вытянутый в бесконечность, с вершиной в центре тяжести области, если смотреть сверху (он же диаграмма Вороного). Затем границы смешиваются для создания конечного продукта.
Будущие улучшения
Размеры Моны Лизы немного отличаются, из-за того, как я храню соотношение сторон. Мне нужно использовать другую систему. Исправлено, предполагая, что исходное соотношение сторон находится где-то между 1:21 и 21: 1, что я считаю разумным предположением.
Гинденбург мог бы быть значительно улучшен. Цветовая палитра, которую я использую, имеет только 6 оттенков серого. Если бы я ввел режим только в оттенках серого, я мог бы использовать дополнительную информацию для увеличения глубины цвета, количества областей, количества растровых точек или любой комбинации этих трех. Я добавил --greyscale
опцию в кодировщик, который выполняет все три.
2d Shapes, вероятно, будет выглядеть лучше с выключенным смешиванием. Я, вероятно, добавлю флаг для этого. Добавлена опция кодировщика для управления коэффициентом сегментации и опция декодера для отключения смешивания.
- Больше веселья с комбинаторикой. 56! на самом деле достаточно большой, чтобы хранить 15 дополнительных регионов и 15! достаточно большой, чтобы хранить еще 2 для общей суммы 73 . Но подождите, это еще не все! Разделение этих 73 объектов также может быть использовано для хранения дополнительной информации. Например, есть 73 выбора 56 способов выбора начальных 56 регионов, а затем 17 выбирают 15 способов выбора следующих 15 . Всего 2403922132944423072 разделов, достаточно больших, чтобы хранить еще 3 региона, в общей сложности 76, Мне нужно было бы придумать умный способ уникального нумерации всех разбиений 73 в группы по 56 , 15 , 2 ... и обратно . Возможно, не практичный, но интересный вопрос для размышления.
0VW*`Gnyq;c1JBY}tj#rOcKm)v_Ac\S.r[>,Xd_(qT6 >]!xOfU9~0jmIMG{hcg-'*a.s<X]6*%U5>/FOze?cPv@hI)PjpK9\iA7P ]a-7eC&ttS[]K>NwN-^$T1E.1OH^c0^"J 4V9X
0Jc?NsbD#1WDuqT]AJFELu<!iE3d!BB>jOA'L|<j!lCWXkr:gCXuD=D\BL{gA\ 8#*RKQ*tv\\3V0j;_4|o7>{Xage-N85):Q/Hl4.t&'0pp)d|Ry+?|xrA6u&2E!Ls]i]T<~)58%RiA
а также
4PV 9G7X|}>pC[Czd!5&rA5 Eo1Q\+m5t:r#;H65NIggfkw'h4*gs.:~<bt'VuVL7V8Ed5{`ft7e>HMHrVVUXc.{#7A|#PBm,i>1B781.K8>s(yUV?a<*!mC@9p+Rgd<twZ.wuFnN dp
Второй кодируется с --greyscale
опцией.
3dVY3TY?9g+b7!5n`)l"Fg H$ 8n?[Q-4HE3.c:[pBBaH`5'MotAj%a4rIodYO.lp$h a94$n!M+Y?(eAR,@Y*LiKnz%s0rFpgnWy%!zV)?SuATmc~-ZQardp=?D5FWx;v=VA+]EJ(:%
Закодировано с --greyscale
опцией.
.9l% Ge<'_)3(`DTsH^eLn|l3.D_na,,sfcpnp{"|lSv<>}3b})%m2M)Ld{YUmf<Uill,*:QNGk,'f2; !2i88T:Yjqa8\Ktz4i@h2kHeC|9,P` v7Xzd Yp&z:'iLra&X&-b(g6vMq
Закодировано с --ratio 60
, и расшифровано с --no-blending
вариантами.
encoder.py
from __future__ import division
import argparse, numpy
from skimage.io import imread
from skimage.transform import resize
from skimage.segmentation import slic
from skimage.measure import regionprops
from my_geom import *
def encode(filename, seg_ratio, greyscale):
img = imread(filename)
height = len(img)
width = len(img[0])
ratio = width/height
if greyscale:
raster_size = 558
raster_ratio = 11
num_segs = 94
set1_len = 70
max_num = 8928 # 558 * 16
else:
raster_size = 402
raster_ratio = 13
num_segs = 70
set1_len = 56
max_num = 86832 # 402 * 216
raster_width = (raster_size*ratio)**0.5
raster_height = int(raster_width/ratio)
raster_width = int(raster_width)
resize_height = raster_height * raster_ratio
resize_width = raster_width * raster_ratio
img = resize(img, (resize_height, resize_width))
segs = slic(img, n_segments=num_segs-4, ratio=seg_ratio).astype('int16')
max_label = segs.max()
numpy.place(segs, segs==0, [max_label+1])
regions = [None]*(max_label+2)
for props in regionprops(segs):
label = props['Label']
props['Greyscale'] = greyscale
regions[label] = Region(props)
for i, a in enumerate(regions):
for j, b in enumerate(regions):
if a==None or b==None or a==b: continue
if a.centroid == b.centroid:
numpy.place(segs, segs==j, [i])
regions[j] = None
for y in range(resize_height):
for x in range(resize_width):
label = segs[y][x]
regions[label].add_point(img[y][x])
regions = [r for r in regions if r != None]
if len(regions)>num_segs:
regions = sorted(regions, key=lambda r: r.area)[-num_segs:]
regions = sorted(regions, key=lambda r: r.to_num(raster_width))
set1, set2 = regions[-set1_len:], regions[:-set1_len]
set2_num = 0
for s in set2:
set2_num *= max_num
set2_num += s.to_num(raster_width)
set2_num = ((set2_num*85 + raster_width)*85 + raster_height)*25 + len(set2)
perm = num2perm(set2_num, set1_len)
set1 = permute(set1, perm)
outnum = 0
for r in set1:
outnum *= max_num
outnum += r.to_num(raster_width)
outnum *= 2
outnum += greyscale
outstr = ''
for i in range(140):
outstr = chr(32 + outnum%95) + outstr
outnum //= 95
print outstr
parser = argparse.ArgumentParser(description='Encodes an image into a tweetable format.')
parser.add_argument('filename', type=str,
help='The filename of the image to encode.')
parser.add_argument('--ratio', dest='seg_ratio', type=float, default=30,
help='The segmentation ratio. Higher values (50+) will result in more regular shapes, lower values in more regular region color.')
parser.add_argument('--greyscale', dest='greyscale', action='store_true',
help='Encode the image as greyscale.')
args = parser.parse_args()
encode(args.filename, args.seg_ratio, args.greyscale)
decoder.py
from __future__ import division
import argparse
from PIL import Image, ImageDraw, ImageChops, ImageFilter
from my_geom import *
def decode(instr, no_blending=False):
innum = 0
for c in instr:
innum *= 95
innum += ord(c) - 32
greyscale = innum%2
innum //= 2
if greyscale:
max_num = 8928
set1_len = 70
image_mode = 'L'
default_color = 0
raster_ratio = 11
else:
max_num = 86832
set1_len = 56
image_mode = 'RGB'
default_color = (0, 0, 0)
raster_ratio = 13
nums = []
for i in range(set1_len):
nums = [innum%max_num] + nums
innum //= max_num
set2_num = perm2num(nums)
set2_len = set2_num%25
set2_num //= 25
raster_height = set2_num%85
set2_num //= 85
raster_width = set2_num%85
set2_num //= 85
resize_width = raster_width*raster_ratio
resize_height = raster_height*raster_ratio
for i in range(set2_len):
nums += set2_num%max_num,
set2_num //= max_num
regions = []
for num in nums:
r = Region()
r.from_num(num, raster_width, greyscale)
regions += r,
masks = []
outimage = Image.new(image_mode, (resize_width, resize_height), default_color)
for a in regions:
mask = Image.new('L', (resize_width, resize_height), 255)
for b in regions:
if a==b: continue
submask = Image.new('L', (resize_width, resize_height), 0)
poly = a.centroid.bisected_poly(b.centroid, resize_width, resize_height)
ImageDraw.Draw(submask).polygon(poly, fill=255, outline=255)
mask = ImageChops.multiply(mask, submask)
outimage.paste(a.avg_color, mask=mask)
if not no_blending:
outimage = outimage.resize((raster_width, raster_height), Image.ANTIALIAS)
outimage = outimage.resize((resize_width, resize_height), Image.BICUBIC)
smooth = ImageFilter.Kernel((3,3),(1,2,1,2,4,2,1,2,1))
for i in range(20):outimage = outimage.filter(smooth)
outimage.show()
parser = argparse.ArgumentParser(description='Decodes a tweet into and image.')
parser.add_argument('--no-blending', dest='no_blending', action='store_true',
help="Do not blend the borders in the final image.")
args = parser.parse_args()
instr = raw_input()
decode(instr, args.no_blending)
my_geom.py
from __future__ import division
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
self.xy = (x, y)
def __eq__(self, other):
return self.x == other.x and self.y == other.y
def __lt__(self, other):
return self.y < other.y or (self.y == other.y and self.x < other.x)
def inv_slope(self, other):
return (other.x - self.x)/(self.y - other.y)
def midpoint(self, other):
return Point((self.x + other.x)/2, (self.y + other.y)/2)
def dist2(self, other):
dx = self.x - other.x
dy = self.y - other.y
return dx*dx + dy*dy
def bisected_poly(self, other, resize_width, resize_height):
midpoint = self.midpoint(other)
points = []
if self.y == other.y:
points += (midpoint.x, 0), (midpoint.x, resize_height)
if self.x < midpoint.x:
points += (0, resize_height), (0, 0)
else:
points += (resize_width, resize_height), (resize_width, 0)
return points
elif self.x == other.x:
points += (0, midpoint.y), (resize_width, midpoint.y)
if self.y < midpoint.y:
points += (resize_width, 0), (0, 0)
else:
points += (resize_width, resize_height), (0, resize_height)
return points
slope = self.inv_slope(other)
y_intercept = midpoint.y - slope*midpoint.x
if self.y > midpoint.y:
points += ((resize_height - y_intercept)/slope, resize_height),
if slope < 0:
points += (resize_width, slope*resize_width + y_intercept), (resize_width, resize_height)
else:
points += (0, y_intercept), (0, resize_height)
else:
points += (-y_intercept/slope, 0),
if slope < 0:
points += (0, y_intercept), (0, 0)
else:
points += (resize_width, slope*resize_width + y_intercept), (resize_width, 0)
return points
class Region:
def __init__(self, props={}):
if props:
self.greyscale = props['Greyscale']
self.area = props['Area']
cy, cx = props['Centroid']
if self.greyscale:
self.centroid = Point(int(cx/11)*11+5, int(cy/11)*11+5)
else:
self.centroid = Point(int(cx/13)*13+6, int(cy/13)*13+6)
self.num_pixels = 0
self.r_total = 0
self.g_total = 0
self.b_total = 0
def __lt__(self, other):
return self.centroid < other.centroid
def add_point(self, rgb):
r, g, b = rgb
self.r_total += r
self.g_total += g
self.b_total += b
self.num_pixels += 1
if self.greyscale:
self.avg_color = int((3.2*self.r_total + 10.7*self.g_total + 1.1*self.b_total)/self.num_pixels + 0.5)*17
else:
self.avg_color = (
int(5*self.r_total/self.num_pixels + 0.5)*51,
int(5*self.g_total/self.num_pixels + 0.5)*51,
int(5*self.b_total/self.num_pixels + 0.5)*51)
def to_num(self, raster_width):
if self.greyscale:
raster_x = int((self.centroid.x - 5)/11)
raster_y = int((self.centroid.y - 5)/11)
return (raster_y*raster_width + raster_x)*16 + self.avg_color//17
else:
r, g, b = self.avg_color
r //= 51
g //= 51
b //= 51
raster_x = int((self.centroid.x - 6)/13)
raster_y = int((self.centroid.y - 6)/13)
return (raster_y*raster_width + raster_x)*216 + r*36 + g*6 + b
def from_num(self, num, raster_width, greyscale):
self.greyscale = greyscale
if greyscale:
self.avg_color = num%16*17
num //= 16
raster_x, raster_y = num%raster_width, num//raster_width
self.centroid = Point(raster_x*11 + 5, raster_y*11+5)
else:
rgb = num%216
r, g, b = rgb//36, rgb//6%6, rgb%6
self.avg_color = (r*51, g*51, b*51)
num //= 216
raster_x, raster_y = num%raster_width, num//raster_width
self.centroid = Point(raster_x*13 + 6, raster_y*13 + 6)
def perm2num(perm):
num = 0
size = len(perm)
for i in range(size):
num *= size-i
for j in range(i, size): num += perm[j]<perm[i]
return num
def num2perm(num, size):
perm = [0]*size
for i in range(size-1, -1, -1):
perm[i] = int(num%(size-i))
num //= size-i
for j in range(i+1, size): perm[j] += perm[j] >= perm[i]
return perm
def permute(arr, perm):
size = len(arr)
out = [0] * size
for i in range(size):
val = perm[i]
out[i] = arr[val]
return out