قبل عامين ، كتبت تنفيذًا بسيطًا للغاية لضغط الصورة النمطي للركاب لعمل الطلاب ونشرت الكود على
جيثب .
لدهشتي ، تبين أن المستودع كان شائعًا للغاية ، لذلك قررت تحديث الكود وكتابة مقالة تشرح ذلك والنظرية.
نظرية
هذا الجزء نظري تمامًا ، وإذا كنت مهتمًا فقط بمستندات الشفرة ، فيمكنك تخطيها.
تعيينات ضغط
سمح
(E،د) هو
الفضاء المتري الكامل ، و
f:E rightarrowE - رسم الخرائط من
E في
E .
نحن نقول ذلك
و هو
تعيين الضغط إذا كان موجودا
0<s<1 بحيث:
forallx،y inE،d(f(x)،f(y)) leqsd(x،y)
بناء على هذا ،
و ستشير إلى تعيين ضغط مع نسبة ضغط
s .
هناك نظريان مهمان حول تعيينات التعاقد:
نظرية Banach للنقطة الثابتة ونظرية الكولاج .
نظرية النقطة الثابتة :
و لديه نقطة ثابتة فريدة من نوعها
x0دولا .
إظهار الدليلأولاً نثبت أن التسلسل
(un) تعيين ك
$ inline $ \ left \ {\ start {alignat *} {2} u_0 & = x \\ u_ {n + 1} & = f (u_n) \ end {alignat *} \ right. $ inline $ متقاربة للجميع
x فيE .
للجميع
m<n in mathbbN :
\ تبدأ {alignat *} {2} d (u_m، u_n) & = d (f ^ m (x)، f ^ n (x)) \\ & \ leq s ^ md (x، f ^ {nm} (x)) \ text {since} f \ text {عبارة عن خريطة انكماش} \\ & \ leq s ^ m \ left (\ sum_ {i = 0} ^ {nm-1} {d (f ^ i (x ) ، f ^ {i + 1} (x))} \ right) \ text {من عدم المساواة المثلث} \\ & \ leq s ^ m \ left (\ sum_ {i = 0} ^ {nm-1} {s ^ id (x، f (x))} \ right) \ text {since} f \ text {هي خريطة انكماش} \\ & = s ^ m \ left (\ frac {1 - s ^ {nm}} {1 - s} d (x، f (x)) \ right) \\ & \ leq \ frac {s ^ m} {1 - s} d (x، f (x)) \ underset {m \ rightarrow \ infty} {\ rightarrow} 0 \ end {alignat *}
لذلك،
(un) هو
تسلسل كوشي ، و
E هو الفضاء الكامل ، وهو ما يعني
(un) متقارب فليكن لها الحد
x0دولا .
علاوة على ذلك ، نظرًا لأن خريطة الانكماش
مستمرة كخريطة Lipschitz ، فهي أيضًا مستمرة ، أي
f(un) rightarrowf(x0) . لذلك ، إذا
ن يميل إلى ما لا نهاية في
un+1=f(un) نحن نحصل عليها
x0=f(x0) . هذا هو
x0دولا هي نقطة ثابتة
و .
لقد أظهرنا ذلك
و لديه نقطة ثابتة. دعونا نظهر بالتناقض أنها فريدة من نوعها. سمح
y0دولا - نقطة ثابتة أخرى. ثم:
d(x0،y0)=d(f(x0)،f(y0)) leqsd(x0،y0)<d(x0،y0)
كان هناك تناقض.
كذلك سوف نشير إلى
x0دولا نقطة ثابتة
و .
نظرية الكولاج : if
d(x،f(x))< epsilon ثم
d(x،x0)< frac epsilon1−s .
إظهار الدليلفي الدليل السابق ، أظهرنا ذلك d(um،un) leq fracsm1−sd(x،f(x))= fracsm1−s epsilon .
إذا كنا إصلاح مدولا في 0 ثم وصلنا d(x،un) leq frac epsilon1−s .
عندما تسعى ن إلى ما لا نهاية نحصل على النتيجة المرجوة.
تخبرنا النظرية الثانية أننا إذا وجدنا خريطة انكماش
و مثل هذا
f(x) قريب من
x ، ثم يمكننا أن نكون متأكدين من أن النقطة الثابتة
و أيضا قريبة من
x .
هذه النتيجة ستكون الأساس لعملنا في المستقبل. في الواقع ، بدلاً من حفظ الصورة ، يكفي أن نحفظ شاشة الضغط ، التي تكون النقطة الثابتة قريبة من الصورة.
يعرض ضغط للصور
سأوضح في هذا الجزء كيفية إنشاء عروض الضغط هذه بحيث تكون النقطة الثابتة قريبة من الصورة.
أولاً ، دعنا نضبط مجموعة الصور والمسافة. سوف نختار
E=[0،1]h timesw .
E هو الكثير من المصفوفات مع
ح في الصفوف
w الأعمدة ومع المعاملات في الفاصل الزمني
[0،1] . ثم سنتخذ
d(x،y)= left( sumhi=1 sumwj=1(xij−yij)2 right)0.5 .
د هي المسافة التي تم الحصول عليها من
القاعدة Frobenius .
سمح
x فيE هي الصورة التي نريد ضغطها.
سنقوم بتقسيم الصورة إلى كتل مرتين:
- أولاً ، نقسم الصورة إلى كتل محدودة أو فاصلة R1،...،RL . يتم فصل هذه الكتل وتغطي الصورة بأكملها.
- ثم نقسم الصورة إلى كتل من المصادر أو المجالات D1،...،DK . لا يتم فصل هذه الكتل بالضرورة ولا تغطي الصورة بأكملها بالضرورة.
على سبيل المثال ، يمكننا تقسيم الصورة على النحو التالي:
ثم لكل كتلة الفاصل
Rl نختار كتلة المجال
Dkl ورسم الخرائط
fl:[0،1]Dkl rightarrow[0،1]Rl .
بعد ذلك يمكننا تحديد وظيفة
و على النحو التالي:
f(x)ij=fl(xDkl)ij textif(i،j) inRl
موافقة : إذا
fl يتم التعاقد تعيينات ، ثم و
و أيضا رسم الخرائط الضغط.
إظهار الدليلسمح
x،y inE ونفترض أن كل شيء
fl هي تعيينات ضغط مع نسبة ضغط
sl . ثم نحصل على ما يلي:
\ تبدأ {alignat *} {2} d (f (x) ، f (y)) ^ 2 & = \ sum_ {i = 1} ^ {h} {\ sum_ {j = 1} ^ {w} { (f (x) _ {ij} -f (y) _ {ij}) ^ 2}} \ text {بحكم التعريف} d \\ & = \ sum_ {l = 1} ^ L {\ sum _ {(i، j) \ in R_l} {(f (x) _ {ij} -f (y) _ {ij}) ^ 2}} \ text {since} (R_l) \ text {هو قسم} \\ & = \ sum_ {l = 1} ^ L {\ sum _ {(i، j) \ in R_l} {(f_l (x_ {D_ {k_l}}) _ {ij} -f_l (y_ {D_ {k_l}}) _ {ij }) ^ 2}} \ text {بحكم التعريف} f \\ & = \ sum_ {l = 1} ^ L {d (f_l (x_ {D_ {k_l}}) ، f_l (y_ {D_ {k_l}}) ) ^ 2} \ text {بحكم التعريف} d \\ & \ leq \ sum_ {l = 1} ^ L {s_l ^ 2d (x_ {D_ {k_l}}، y_ {D_ {k_l}}) ^ 2} \ text {since} (f_l) \ text {عبارة عن تعيينات انكماش} \\ & \ leq \ underset {l} {\ max} {s_l ^ 2} \ sum_ {l = 1} ^ L {d (x_ {D_ {k_l }} ، y_ {D_ {k_l}}) ^ 2} \\ & = \ underset {l} {\ max} {s_l ^ 2} \ sum_ {l = 1} ^ L {\ sum _ {(i، j) \ في R_l} {(x_ {ij} -y_ {ij}) ^ 2}} \ text {بحكم التعريف} d \\ & = \ underset {l} {\ max} {s_l ^ 2} \ sum_ {i = 1} ^ {h} {\ sum_ {j = 1} ^ {w} {(x_ {ij} -y_ {ij}) ^ 2}} \ text {since} (R_l) \ text {هو قسم} \\ & = \ underset {l} {\ max} {s_l ^ 2} d (x، y) ^ 2 \ text {بحكم التعريف} d \\ \ end {alignat *}
لا يزال هناك سؤال واحد يحتاج إلى إجابة: كيفية الاختيار
Dkl و
fl ؟
توفر نظرية الكولاج طريقة لاختيارها: if
xRl قريب من
f(xDkl) للجميع
ل ثم
x قريب من
f(x) ومن خلال نظرية الكولاج
x و
x0دولا هي أيضا قريبة.
لذلك نحن مستقلون للجميع
ل يمكننا بناء مجموعة من تعيينات الضغط من كل منهما
Dk في
Rl واختيار الأفضل. في القسم التالي ، نعرض جميع تفاصيل هذه العملية.
تطبيق
في كل قسم ، سأقوم بنسخ أجزاء التعليمات البرمجية المثيرة للاهتمام ، ويمكن العثور على البرنامج النصي بأكمله
هنا .
الأقسام
اخترت نهجا بسيطا جدا. تقوم كتل المصادر وكتل الأوراق بتقسيم الصورة على الشبكة ، كما هو موضح في الصورة أعلاه.
حجم الكتل يساوي صلاحيات اثنين وهذا يبسط العمل إلى حد كبير. كتل المصدر هي 8 في 8 وكتل النهاية هي 4 في 4.
هناك خطط التقسيم أكثر تعقيدا. على سبيل المثال ، يمكننا استخدام شجرة quadtree لتفريق المساحات مع الكثير من التفاصيل بقوة.
التحولات
في هذا القسم ، سأعرض كيفية إنشاء تعيينات ضغط من
Dk في
Rl .
تذكر أننا نريد إنشاء مثل هذا التعيين
fl إلى
f(xDk) كان قريبا من
xRl . أي أنه كلما زادت التعيينات التي ننشئها ، زاد احتمال العثور على تعيينات جيدة.
ومع ذلك ، تعتمد جودة الضغط على عدد البتات المطلوبة للتخزين
fl . أي إذا كانت العديد من الوظائف كبيرة جدًا ، فسيكون الضغط سيئًا. هناك حاجة إلى حل وسط هنا.
قررت ذلك
fl سوف تبدو مثل هذا:
fl(xDk)=s timesتدوير theta(flipd(اختزل(xDk)))+ب
حيث

- هذه وظيفة للانتقال من الكتل 8 إلى 8 إلى الكتل 4 إلى 4 ،

و

- التحولات أفيني ،
s يغير التباين و
ب - السطوع.
reduce
وظيفة تقليل حجم الصورة عن طريق حساب متوسط الحي:
def reduce(img, factor): result = np.zeros((img.shape[0] // factor, img.shape[1] // factor)) for i in range(result.shape[0]): for j in range(result.shape[1]): result[i,j] = np.mean(img[i*factor:(i+1)*factor,j*factor:(j+1)*factor]) return result
تقوم وظيفة
rotate
بتدوير الصورة بزاوية معينة:
def rotate(img, angle): return ndimage.rotate(img, angle, reshape=False)
للحفاظ على شكل زاوية الصورة
theta يمكن أن تأخذ فقط القيم
\ {0 ^ {\ circ} ، 90 ^ {\ circ} ، 180 ^ {\ circ} ، 270 ^ {\ circ} \} .
تعكس وظيفة الانعكاس الصورة إذا كان
direction
-1 ولا تنعكس إذا كانت القيمة 1:
def flip(img, direction): return img[::direction,:]
يتم إجراء التحويل الكامل بواسطة وظيفة
apply_transformation
:
def apply_transformation(img, direction, angle, contrast=1.0, brightness=0.0): return contrast*rotate(flip(img, direction), angle) + brightness
نحتاج إلى 1 بت لتتذكر ما إذا كان النسخ المتطابق مطلوبًا و 2 بت لزاوية الدوران. علاوة على ذلك ، إذا أنقذنا
s و
ب باستخدام 8 بتات لكل قيمة ، سنحتاج إلى 11 بت فقط لحفظ التحويل.
بالإضافة إلى ذلك ، يجب علينا التحقق مما إذا كانت هذه الوظائف عبارة عن تعيينات انكماش. والدليل على ذلك ممل قليلاً ، وليس ما نحتاجه حقًا. ربما في وقت لاحق سوف أضيفه كملحق لهذه المادة.
ضغط
خوارزمية الضغط بسيطة. أولاً ، نقوم بإنشاء جميع التحويلات الممكنة لكل مجموعات المصادر باستخدام دالة
generate_all_transformed_blocks
:
def generate_all_transformed_blocks(img, source_size, destination_size, step): factor = source_size // destination_size transformed_blocks = [] for k in range((img.shape[0] - source_size) // step + 1): for l in range((img.shape[1] - source_size) // step + 1):
بعد ذلك ، لكل كتلة نهائية ، نتحقق من جميع كتل المصادر المحولة التي تم إنشاؤها مسبقًا. لكل منها ، نقوم بتحسين التباين والسطوع باستخدام طريقة
find_contrast_and_brightness2
، وإذا كان التحويل الذي تم اختباره هو الأفضل للجميع ، تم حفظه:
def compress(img, source_size, destination_size, step): transformations = [] transformed_blocks = generate_all_transformed_blocks(img, source_size, destination_size, step) for i in range(img.shape[0] // destination_size): transformations.append([]) for j in range(img.shape[1] // destination_size): print(i, j) transformations[i].append(None) min_d = float('inf')
للعثور على أفضل التباين والسطوع ، فإن طريقة
find_contrast_and_brightness2
تحل ببساطة مشكلة المربعات الصغرى:
def find_contrast_and_brightness2(D, S):
تفريغ
خوارزمية تخفيف الضغط أكثر بساطة. نبدأ مع صورة عشوائية تمامًا ، ثم نطبق صورة مضغوطة عدة مرات
و :
def decompress(transformations, source_size, destination_size, step, nb_iter=8): factor = source_size // destination_size height = len(transformations) * destination_size width = len(transformations[0]) * destination_size iterations = [np.random.randint(0, 256, (height, width))] cur_img = np.zeros((height, width)) for i_iter in range(nb_iter): print(i_iter) for i in range(len(transformations)): for j in range(len(transformations[i])):
تعمل هذه الخوارزمية لأن خريطة الضغط تحتوي على نقطة ثابتة فريدة من نوعها ، وبغض النظر عن الصورة المصدر التي نختارها ، سنسعى جاهدين لتحقيقها.
أعتقد أن الوقت قد حان لمثال صغير. سأحاول ضغط وفك ضغط صورة القرد:
تقوم دالة
test_greyscale
بتحميل الصورة وضغطها
test_greyscale
كل تكرار لضغط الضغط:
ليس سيئا على الإطلاق لمثل هذا التنفيذ البسيط!
صور RGB
تتمثل الطريقة الساذجة للغاية لضغط صور RGB في ضغط القنوات الثلاث كل على حدة:
def compress_rgb(img, source_size, destination_size, step): img_r, img_g, img_b = extract_rgb(img) return [compress(img_r, source_size, destination_size, step), \ compress(img_g, source_size, destination_size, step), \ compress(img_b, source_size, destination_size, step)]
ولإلغاء التفريغ ، نقوم ببساطة بفك بيانات ثلاث قنوات بشكل منفصل ونجمعها في ثلاث قنوات للصور:
def decompress_rgb(transformations, source_size, destination_size, step, nb_iter=8): img_r = decompress(transformations[0], source_size, destination_size, step, nb_iter)[-1] img_g = decompress(transformations[1], source_size, destination_size, step, nb_iter)[-1] img_b = decompress(transformations[2], source_size, destination_size, step, nb_iter)[-1] return assemble_rbg(img_r, img_g, img_b)
حل آخر بسيط للغاية هو استخدام نفس عرض الضغط لجميع القنوات الثلاث ، لأنها غالباً ما تكون متشابهة للغاية.
إذا كنت تريد التحقق من كيفية عمل ذلك ،
test_rgb
بتشغيل الدالة
test_rgb
:
ظهرت القطع الأثرية. ربما تكون هذه الطريقة ساذجة جدًا لتحقيق نتائج جيدة.
إلى أين أذهب بعد ذلك
إذا كنت تريد معرفة المزيد عن ضغط الصورة النمطي هندسيًا ، يمكنني أن أنصحك بقراءة مقالة
Fractal and Wavelet Image Compression Techniques by Stephen Welsted. إنه سهل القراءة ويشرح تقنيات أكثر تطوراً.