دليل شامل لفهم وتطبيق خوارزميات ضغط الفيديو من الصفر باستخدام بايثون. تعلم النظرية والممارسة وراء برامج ترميز الفيديو الحديثة.
بناء برنامج ترميز فيديو في بايثون: غوص عميق في خوارزميات الضغط
في عالمنا شديد الاتصال، الفيديو هو الملك. من خدمات البث ومؤتمرات الفيديو إلى خلاصات وسائل التواصل الاجتماعي، يهيمن الفيديو الرقمي على حركة مرور الإنترنت. ولكن كيف يمكن إرسال فيلم عالي الدقة عبر اتصال إنترنت قياسي؟ تكمن الإجابة في مجال رائع ومعقد: ضغط الفيديو. في قلب هذه التقنية يوجد برنامج ترميز الفيديو (COder-DECoder)، وهو مجموعة متطورة من الخوارزميات المصممة لتقليل حجم الملف بشكل جذري مع الحفاظ على الجودة البصرية.
في حين أن برامج الترميز القياسية في الصناعة مثل H.264 و HEVC (H.265) و AV1 الخالية من حقوق الملكية هي قطع هندسية معقدة بشكل لا يصدق، فإن فهم مبادئها الأساسية متاح لأي مطور متحفز. سيأخذك هذا الدليل في رحلة عميقة إلى عالم ضغط الفيديو. لن نتحدث عن النظرية فحسب؛ بل سنقوم ببناء برنامج ترميز فيديو تعليمي مبسط من الألف إلى الياء باستخدام بايثون. هذا النهج العملي هو أفضل طريقة لاستيعاب الأفكار الأنيقة التي تجعل بث الفيديو الحديث ممكنًا.
لماذا بايثون؟ بينما ليست اللغة التي قد تستخدمها لبرنامج ترميز تجاري عالي الأداء في الوقت الفعلي (والتي تُكتب عادةً بلغة C/C++ أو حتى التجميع)، فإن سهولة قراءة بايثون ومكتباتها القوية مثل NumPy و SciPy و OpenCV تجعلها البيئة المثالية للتعلم والنماذج الأولية والبحث. يمكنك التركيز على الخوارزميات دون الانغماس في إدارة الذاكرة منخفضة المستوى.
فهم المفاهيم الأساسية لضغط الفيديو
قبل أن نكتب سطرًا واحدًا من التعليمات البرمجية، يجب أن نفهم ما نحاول تحقيقه. الهدف من ضغط الفيديو هو إزالة البيانات الزائدة. الفيديو الخام غير المضغوط ضخم للغاية. يمكن لدقيقة واحدة من فيديو بدقة 1080p بمعدل 30 إطارًا في الثانية أن تتجاوز 7 جيجابايت. للسيطرة على وحش البيانات هذا، نستغل نوعين أساسيين من التكرار.
ركيزتا الضغط: التكرار المكاني والزماني
- التكرار المكاني (داخل الإطار): هذا هو التكرار داخل إطار واحد. فكر في مساحة كبيرة من السماء الزرقاء أو جدار أبيض. بدلاً من تخزين قيمة اللون لكل بكسل في تلك المنطقة، يمكننا وصفها بكفاءة أكبر. هذا هو نفس المبدأ وراء تنسيقات ضغط الصور مثل JPEG.
- التكرار الزماني (بين الإطارات): هذا هو التكرار بين الإطارات المتتالية. في معظم مقاطع الفيديو، لا يتغير المشهد بالكامل من إطار إلى آخر. على سبيل المثال، شخص يتحدث أمام خلفية ثابتة لديه كميات هائلة من التكرار الزماني. تبقى الخلفية كما هي؛ يتحرك جزء صغير فقط من الصورة (وجه الشخص وجسمه). هذا هو أهم مصدر للضغط في الفيديو.
أنواع الإطارات الرئيسية: الإطارات I و P و B
لاستغلال التكرار الزماني، لا تتعامل برامج الترميز مع كل إطار على قدم المساواة. تقوم بتصنيفها إلى أنواع مختلفة، لتشكل تسلسلًا يسمى مجموعة الصور (GOP).
- إطار I (إطار مشفر داخليًا): إطار I هو صورة كاملة ومكتفية ذاتيًا. يتم ضغطها باستخدام التكرار المكاني فقط، تمامًا مثل JPEG. تعمل إطارات I كنقاط ارتكاز في دفق الفيديو، مما يسمح للمشاهد ببدء التشغيل أو البحث عن موضع جديد. إنها أكبر أنواع الإطارات ولكنها ضرورية لإعادة إنشاء الفيديو.
- إطار P (إطار متوقع): يتم تشفير إطار P بالنظر إلى الإطار I أو P السابق. بدلاً من تخزين الصورة بأكملها، فإنه يخزن الفروق فقط. على سبيل المثال، يخزن تعليمات مثل "خذ هذه الكتلة من البكسلات من الإطار الأخير، حركها 5 بكسلات إلى اليمين، وهنا التغييرات اللونية الطفيفة." يتم تحقيق ذلك من خلال عملية تسمى تقدير الحركة.
- إطار B (إطار متوقع ثنائي الاتجاه): إطار B هو الأكثر كفاءة. يمكنه استخدام كل من الإطار السابق والإطار التالي كمراجع للتنبؤ. هذا مفيد للمشاهد التي يتم فيها إخفاء كائن مؤقتًا ثم يظهر مرة أخرى. من خلال النظر إلى الأمام والخلف، يمكن لبرنامج الترميز إنشاء تنبؤ أكثر دقة وكفاءة في استخدام البيانات. ومع ذلك، فإن استخدام الإطارات المستقبلية يُحدث تأخيرًا صغيرًا (زمن استجابة)، مما يجعلها أقل ملاءمة للتطبيقات في الوقت الفعلي مثل مكالمات الفيديو.
قد تبدو مجموعة صور GOP نموذجية كالتالي: I B B P B B P B B I .... يقرر المرمز النمط الأمثل للإطارات لموازنة كفاءة الضغط وسهولة البحث.
خط أنابيب الضغط: تحليل خطوة بخطوة
ترميز الفيديو الحديث هو خط أنابيب متعدد المراحل. تحول كل مرحلة البيانات لجعلها قابلة للضغط بشكل أكبر. دعنا نلقي نظرة على الخطوات الرئيسية لترميز إطار واحد.

الخطوة 1: تحويل مساحة الألوان (RGB إلى YCbCr)
تبدأ معظم مقاطع الفيديو في مساحة الألوان RGB (الأحمر والأخضر والأزرق). ومع ذلك، فإن العين البشرية أكثر حساسية للتغيرات في السطوع (luma) منها للتغيرات في اللون (chroma). تستغل برامج الترميز ذلك عن طريق تحويل RGB إلى تنسيق لومينا/كروما مثل YCbCr.
- Y: مكون اللومينا (السطوع).
- Cb: مكون الكروما ذو الفرق الأزرق.
- Cr: مكون الكروما ذو الفرق الأحمر.
عن طريق فصل السطوع عن اللون، يمكننا تطبيق التصغير اللوني (chroma subsampling). تقلل هذه التقنية من دقة قنوات الألوان (Cb و Cr) مع الاحتفاظ بالدقة الكاملة لقناة السطوع (Y)، التي تكون عيوننا أكثر حساسية لها. مخطط شائع هو 4:2:0، والذي يتخلص من 75% من معلومات اللون دون أي فقدان ملحوظ في الجودة تقريبًا، مما يحقق ضغطًا فوريًا.
الخطوة 2: تقسيم الإطار (Macroblocks)
لا يعالج المرمز الإطار بأكمله دفعة واحدة. يقسم الإطار إلى كتل أصغر، عادة 16×16 أو 8×8 بكسل، تسمى Macroblocks. يتم تنفيذ جميع خطوات المعالجة اللاحقة (التنبؤ، التحويل، إلخ) على أساس كل كتلة على حدة.
الخطوة 3: التنبؤ (بين الإطارات وداخل الإطار)
هنا يحدث السحر. لكل Macroblock، يقرر المرمز ما إذا كان سيستخدم التنبؤ داخل الإطار أو بين الإطارات.
- بالنسبة لإطار I (التنبؤ داخل الإطار): يتنبأ المرمز بالكتلة الحالية بناءً على وحدات البكسل لجيرانها المشفرة بالفعل (الكتل أعلاه وعلى اليسار) داخل الإطار نفسه. ثم يحتاج فقط إلى تشفير الفرق الصغير (البقايا) بين التنبؤ والكتلة الفعلية.
- بالنسبة لإطار P أو إطار B (التنبؤ بين الإطارات): هذا هو تقدير الحركة. يبحث المرمز عن كتلة مطابقة في إطار مرجعي. عندما يجد أفضل تطابق، يسجل متجه حركة (على سبيل المثال، "تحريك 10 بكسلات يمينًا، 2 بكسل لأسفل") ويحسب البقايا. غالبًا ما تكون البقايا قريبة من الصفر، مما يتطلب عددًا قليلاً جدًا من البتات لتشفيرها.
الخطوة 4: التحويل (مثل تحويل جيب التمام المتقطع - DCT)
بعد التنبؤ، لدينا كتلة متبقية. يتم تمرير هذه الكتلة عبر تحويل رياضي مثل تحويل جيب التمام المتقطع (DCT). لا يضغط DCT البيانات بحد ذاتها، ولكنه يغير بشكل أساسي كيفية تمثيلها. يحول قيم البكسل المكانية إلى معاملات تردد. يكمن سحر DCT في أنه بالنسبة لمعظم الصور الطبيعية، فإنه يركز معظم الطاقة المرئية في عدد قليل فقط من المعاملات في الزاوية العلوية اليسرى من الكتلة (مكونات التردد المنخفض)، بينما تكون بقية المعاملات (ضوضاء التردد العالي) قريبة من الصفر.
الخطوة 5: التكميم (Quantization)
هذه هي الخطوة الفقدية الأساسية في خط الأنابيب والمفتاح للتحكم في مفاضلة الجودة مقابل معدل البت. يتم تقسيم الكتلة المحولة من معاملات DCT بواسطة مصفوفة تكميم، ويتم تقريب النتائج إلى أقرب عدد صحيح. تحتوي مصفوفة التكميم على قيم أكبر لمعاملات التردد العالي، مما يؤدي إلى تقليص العديد منها إلى الصفر بشكل فعال. هنا يتم التخلص من كمية هائلة من البيانات. تؤدي معلمة التكميم الأعلى إلى المزيد من الأصفار، وضغط أعلى، وجودة بصرية أقل (تظهر غالبًا على شكل تشوهات كتلية).
الخطوة 6: ترميز الإنتروبيا (Entropy Coding)
المرحلة النهائية هي خطوة ضغط غير فقدي. يتم مسح المعاملات المكممة، ومتجهات الحركة، والبيانات الوصفية الأخرى وتحويلها إلى دفق ثنائي. تستخدم تقنيات مثل ترميز طول التشغيل (RLE) وترميز هوفمان أو طرق أكثر تقدمًا مثل CABAC (الترميز الحسابي الثنائي المتكيف مع السياق). تقوم هذه الخوارزميات بتعيين رموز أقصر للرموز الأكثر تكرارًا (مثل العديد من الأصفار الناتجة عن التكميم) ورموز أطول للرموز الأقل تكرارًا، لاستخراج البتات النهائية من دفق البيانات.
يقوم فك التشفير ببساطة بتنفيذ هذه الخطوات بترتيب عكسي: فك ترميز الإنتروبيا -> فك التكميم العكسي -> التحويل العكسي -> تعويض الحركة -> إعادة بناء الإطار.
تطبيق برنامج ترميز فيديو مبسط في بايثون
الآن، دعنا نضع النظرية موضع التنفيذ. سنقوم ببناء برنامج ترميز تعليمي يستخدم إطارات I وإطارات P. سيوضح خط الأنابيب الأساسي: تقدير الحركة، DCT، التكميم، وخطوات فك التشفير المقابلة.
إخلاء مسؤولية: هذا برنامج ترميز *لعبة* مصمم للتعلم. إنه غير محسن ولن ينتج عنه نتائج قابلة للمقارنة بـ H.264. هدفنا هو رؤية الخوارزميات وهي تعمل.
المتطلبات الأساسية
ستحتاج إلى مكتبات بايثون التالية. يمكنك تثبيتها باستخدام pip:
pip install numpy opencv-python scipy
هيكل المشروع
دعنا ننظم تعليماتنا البرمجية في عدد قليل من الملفات:
main.py: البرنامج النصي الرئيسي لتشغيل عملية التشفير وفك التشفير.encoder.py: يحتوي على منطق المرمز.decoder.py: يحتوي على منطق فك التشفير.utils.py: وظائف مساعدة لمدخلات/مخرجات الفيديو والتحويلات.
الجزء 1: الأدوات المساعدة الأساسية (`utils.py`)
سنبدأ بوظائف مساعدة لـ DCT، والتكميم، وعكسهما. سنحتاج أيضًا إلى وظيفة لتقسيم الإطار إلى كتل.
# utils.py
import numpy as np
from scipy.fftpack import dct, idct
BLOCK_SIZE = 8
# A standard JPEG quantization matrix (scaled for our purposes)
QUANTIZATION_MATRIX = np.array([
[16, 11, 10, 16, 24, 40, 51, 61],
[12, 12, 14, 19, 26, 58, 60, 55],
[14, 13, 16, 24, 40, 57, 69, 56],
[14, 17, 22, 29, 51, 87, 80, 62],
[18, 22, 37, 56, 68, 109, 103, 77],
[24, 35, 55, 64, 81, 104, 113, 92],
[49, 64, 78, 87, 103, 121, 120, 101],
[72, 92, 95, 98, 112, 100, 103, 99]
])
def apply_dct(block):
"""Applies 2D DCT to a block."""
# Center the pixel values around 0
block = block - 128
return dct(dct(block.T, norm='ortho').T, norm='ortho')
def apply_idct(dct_block):
"""Applies 2D Inverse DCT to a block."""
block = idct(idct(dct_block.T, norm='ortho').T, norm='ortho')
# De-center and clip to valid pixel range
return np.round(block + 128).clip(0, 255)
def quantize(dct_block, qp=1):
"""Quantizes a DCT block. qp is a quality parameter."""
return np.round(dct_block / (QUANTIZATION_MATRIX * qp)).astype(int)
def dequantize(quantized_block, qp=1):
"""Dequantizes a block."""
return quantized_block * (QUANTIZATION_MATRIX * qp)
def frame_to_blocks(frame):
"""Splits a frame into 8x8 blocks."""
blocks = []
h, w = frame.shape
for i in range(0, h, BLOCK_SIZE):
for j in range(0, w, BLOCK_SIZE):
blocks.append(frame[i:i+BLOCK_SIZE, j:j+BLOCK_SIZE])
return blocks
def blocks_to_frame(blocks, h, w):
"""Reconstructs a frame from 8x8 blocks."""
frame = np.zeros((h, w), dtype=np.uint8)
k = 0
for i in range(0, h, BLOCK_SIZE):
for j in range(0, w, BLOCK_SIZE):
frame[i:i+BLOCK_SIZE, j:j+BLOCK_SIZE] = blocks[k]
k += 1
return frame
الجزء 2: المرمز (`encoder.py`)
المرمز هو الجزء الأكثر تعقيدًا. سنقوم بتطبيق خوارزمية مطابقة الكتل البسيطة لتقدير الحركة ثم معالجة الإطارات I والإطارات P.
# encoder.py
import numpy as np
from utils import apply_dct, quantize, frame_to_blocks, BLOCK_SIZE
def get_motion_vectors(current_frame, reference_frame, search_range=8):
"""A simple block matching algorithm for motion estimation."""
h, w = current_frame.shape
motion_vectors = []
for i in range(0, h, BLOCK_SIZE):
for j in range(0, w, BLOCK_SIZE):
current_block = current_frame[i:i+BLOCK_SIZE, j:j+BLOCK_SIZE]
best_match_sad = float('inf')
best_match_vector = (0, 0)
# Search in the reference frame
for y in range(-search_range, search_range + 1):
for x in range(-search_range, search_range + 1):
ref_i, ref_j = i + y, j + x
if 0 <= ref_i <= h - BLOCK_SIZE and 0 <= ref_j <= w - BLOCK_SIZE:
ref_block = reference_frame[ref_i:ref_i+BLOCK_SIZE, ref_j:ref_j+BLOCK_SIZE]
sad = np.sum(np.abs(current_block - ref_block))
if sad < best_match_sad:
best_match_sad = sad
best_match_vector = (y, x)
motion_vectors.append(best_match_vector)
return motion_vectors
def encode_iframe(frame, qp=1):
"""Encodes an I-frame."""
h, w = frame.shape
blocks = frame_to_blocks(frame)
quantized_blocks = []
for block in blocks:
dct_block = apply_dct(block.astype(float))
quantized_block = quantize(dct_block, qp)
quantized_blocks.append(quantized_block)
return {'type': 'I', 'h': h, 'w': w, 'data': quantized_blocks, 'qp': qp}
def encode_pframe(current_frame, reference_frame, qp=1):
"""Encodes a P-frame."""
h, w = current_frame.shape
motion_vectors = get_motion_vectors(current_frame, reference_frame)
quantized_residuals = []
k = 0
for i in range(0, h, BLOCK_SIZE):
for j in range(0, w, BLOCK_SIZE):
current_block = current_frame[i:i+BLOCK_SIZE, j:j+BLOCK_SIZE]
mv_y, mv_x = motion_vectors[k]
ref_block = reference_frame[i+mv_y : i+mv_y+BLOCK_SIZE, j+mv_x : j+mv_x+BLOCK_SIZE]
residual = current_block.astype(float) - ref_block.astype(float)
dct_residual = apply_dct(residual)
quantized_residual = quantize(dct_residual, qp)
quantized_residuals.append(quantized_residual)
k += 1
return {'type': 'P', 'motion_vectors': motion_vectors, 'data': quantized_residuals, 'qp': qp}
الجزء 3: فك التشفير (`decoder.py`)
يقوم فك التشفير بعكس العملية. بالنسبة لإطارات P، يقوم بإجراء تعويض الحركة باستخدام متجهات الحركة المخزنة.
# decoder.py
import numpy as np
from utils import apply_idct, dequantize, blocks_to_frame, BLOCK_SIZE
def decode_iframe(encoded_frame):
"""Decodes an I-frame."""
h, w = encoded_frame['h'], encoded_frame['w']
qp = encoded_frame['qp']
quantized_blocks = encoded_frame['data']
reconstructed_blocks = []
for q_block in quantized_blocks:
dct_block = dequantize(q_block, qp)
block = apply_idct(dct_block)
reconstructed_blocks.append(block.astype(np.uint8))
return blocks_to_frame(reconstructed_blocks, h, w)
def decode_pframe(encoded_frame, reference_frame):
"""Decodes a P-frame using its reference frame."""
h, w = reference_frame.shape
qp = encoded_frame['qp']
motion_vectors = encoded_frame['motion_vectors']
quantized_residuals = encoded_frame['data']
reconstructed_blocks = []
k = 0
for i in range(0, h, BLOCK_SIZE):
for j in range(0, w, BLOCK_SIZE):
# Decode the residual
dct_residual = dequantize(quantized_residuals[k], qp)
residual = apply_idct(dct_residual)
# Perform motion compensation
mv_y, mv_x = motion_vectors[k]
ref_block = reference_frame[i+mv_y : i+mv_y+BLOCK_SIZE, j+mv_x : j+mv_x+BLOCK_SIZE]
# Reconstruct the block
reconstructed_block = (ref_block.astype(float) + residual).clip(0, 255)
reconstructed_blocks.append(reconstructed_block.astype(np.uint8))
k += 1
return blocks_to_frame(reconstructed_blocks, h, w)
الجزء 4: تجميع كل شيء (`main.py`)
ينظم هذا النص البرمجي العملية بأكملها: قراءة الفيديو، ترميزه إطارًا بإطار، ثم فك ترميزه لإنتاج مخرجات نهائية.
# main.py
import cv2
import pickle # For saving/loading our compressed data structure
from encoder import encode_iframe, encode_pframe
from decoder import decode_iframe, decode_pframe
def main(input_path, output_path, compressed_file_path):
cap = cv2.VideoCapture(input_path)
frames = []
while True:
ret, frame = cap.read()
if not ret:
break
# We'll work with the grayscale (luma) channel for simplicity
frames.append(cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY))
cap.release()
# --- ENCODING --- #
print("Encoding...")
compressed_data = []
reference_frame = None
gop_size = 12 # I-frame every 12 frames
for i, frame in enumerate(frames):
if i % gop_size == 0:
# Encode as I-frame
encoded_frame = encode_iframe(frame, qp=2.5)
compressed_data.append(encoded_frame)
print(f"Encoded frame {i} as I-frame")
else:
# Encode as P-frame
encoded_frame = encode_pframe(frame, reference_frame, qp=2.5)
compressed_data.append(encoded_frame)
print(f"Encoded frame {i} as P-frame")
# The reference for the next P-frame needs to be the *reconstructed* last frame
if encoded_frame['type'] == 'I':
reference_frame = decode_iframe(encoded_frame)
else:
reference_frame = decode_pframe(encoded_frame, reference_frame)
with open(compressed_file_path, 'wb') as f:
pickle.dump(compressed_data, f)
print(f"Compressed data saved to {compressed_file_path}")
# --- DECODING --- #
print("\nDecoding...")
with open(compressed_file_path, 'rb') as f:
loaded_compressed_data = pickle.load(f)
decoded_frames = []
reference_frame = None
for i, encoded_frame in enumerate(loaded_compressed_data):
if encoded_frame['type'] == 'I':
decoded_frame = decode_iframe(encoded_frame)
print(f"Decoded frame {i} (I-frame)")
else:
decoded_frame = decode_pframe(encoded_frame, reference_frame)
print(f"Decoded frame {i} (P-frame)")
decoded_frames.append(decoded_frame)
reference_frame = decoded_frame
# --- WRITING OUTPUT VIDEO --- #
h, w = decoded_frames[0].shape
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
out = cv2.VideoWriter(output_path, fourcc, 30.0, (w, h), isColor=False)
for frame in decoded_frames:
out.write(frame)
out.release()
print(f"Decoded video saved to {output_path}")
if __name__ == '__main__':
main('input.mp4', 'output.mp4', 'compressed.bin')
تحليل النتائج واستكشاف المزيد
بعد تشغيل النص البرمجي main.py مع ملف input.mp4، ستحصل على ملفين: compressed.bin، الذي يحتوي على بيانات الفيديو المضغوطة المخصصة لدينا، وoutput.mp4، وهو الفيديو المعاد بناؤه. قارن حجم input.mp4 بـ compressed.bin لرؤية نسبة الضغط. افحص output.mp4 بصريًا لرؤية الجودة. من المرجح أن ترى تشوهات كتلية، خاصة مع قيمة qp أعلى، وهي علامة كلاسيكية للتكميم.
قياس الجودة: نسبة الإشارة إلى الضوضاء القصوى (PSNR)
مقياس موضوعي شائع لقياس جودة إعادة البناء هو PSNR. يقارن الإطار الأصلي بالإطار الذي تم فك ترميزه. تشير قيمة PSNR الأعلى عمومًا إلى جودة أفضل.
import numpy as np
import math
def calculate_psnr(original, compressed):
mse = np.mean((original - compressed) ** 2)
if mse == 0:
return float('inf')
max_pixel = 255.0
psnr = 20 * math.log10(max_pixel / math.sqrt(mse))
return psnr
القيود والخطوات التالية
برنامج الترميز البسيط الخاص بنا هو بداية رائعة، لكنه بعيد عن الكمال. فيما يلي بعض القيود والتحسينات المحتملة التي تعكس تطور برامج الترميز الواقعية:
- تقدير الحركة: بحثنا الشامل بطيء وأساسي. تستخدم برامج الترميز الحقيقية خوارزميات بحث هرمية متطورة للعثور على متجهات الحركة بشكل أسرع بكثير.
- إطارات B: لقد طبقنا فقط إطارات P. ستؤدي إضافة إطارات B إلى تحسين كفاءة الضغط بشكل كبير على حساب زيادة التعقيد وزمن الاستجابة.
- ترميز الإنتروبيا: لم نقم بتطبيق مرحلة ترميز إنتروبيا مناسبة. لقد قمنا ببساطة بتخزين هياكل بيانات بايثون. ستؤدي إضافة برنامج ترميز طول التشغيل (Run-Length Encoder) للأصفار المكممة، متبوعًا بترميز هوفمان أو الترميز الحسابي، إلى تقليل حجم الملف بشكل أكبر.
- فلتر إزالة التكتل (Deblocking Filter): تسبب الحواف الحادة بين كتلنا 8×8 تشوهات مرئية. تطبق برامج الترميز الحديثة فلتر إزالة التكتل بعد إعادة البناء لتنعيم هذه الحواف وتحسين الجودة البصرية.
- أحجام الكتل المتغيرة: لا تستخدم برامج الترميز الحديثة كتل ماكرو ثابتة بحجم 16×16 فقط. يمكنها تقسيم الإطار بشكل تكيفي إلى أحجام وأشكال كتل مختلفة لتناسب المحتوى بشكل أفضل (على سبيل المثال، استخدام كتل أكبر للمناطق المسطحة وكتل أصغر للمناطق التفصيلية).
الخاتمة
يعد بناء برنامج ترميز فيديو، حتى لو كان مبسطًا، تمرينًا مجزيًا للغاية. إنه يزيل الغموض عن التكنولوجيا التي تشغل جزءًا كبيرًا من حياتنا الرقمية. لقد سافرنا عبر المفاهيم الأساسية للتكرار المكاني والزماني، وتتبعنا المراحل الأساسية لخط أنابيب التشفير - التنبؤ والتحويل والتكميم - وقمنا بتطبيق هذه الأفكار في بايثون.
التعليمات البرمجية المقدمة هنا هي نقطة بداية. أشجعك على تجربتها. حاول تغيير حجم الكتلة، أو معلمة التكميم (qp)، أو طول مجموعة الصور (GOP). حاول تطبيق مخطط ترميز طول التشغيل البسيط أو حتى مواجهة تحدي إضافة إطارات B. من خلال البناء والتفكيك، ستكتسب تقديرًا عميقًا للبراعة وراء تجارب الفيديو السلسة التي غالبًا ما نعتبرها أمرًا مفروغًا منه. عالم ضغط الفيديو واسع ويتطور باستمرار، مما يوفر فرصًا لا حصر لها للتعلم والابتكار.