পাঠ ৪১ · ৫৭-এর মধ্যে · মডিউল ৯
Home / Courses / Digital Signal Processing / আপস্যাম্পলিং

আপস্যাম্পলিং (ইন্টারপোলেশন)

Upsampling (interpolation)
১০ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • জিরো-ইনসার্শনের মাধ্যমে সিগন্যালকে L গুণ লম্বা করার প্রক্রিয়া
  • কেন জিরো-স্টাফড সিগন্যাল সরাসরি ব্যবহারযোগ্য নয়, এবং লো-পাস ফিল্টারিং কীভাবে এটিকে প্রকৃত ইন্টারপোলেটেড সিগন্যালে রূপান্তর করে
  • গেইন সংশোধন (×L) কেন দরকার তা গণনা দিয়ে দেখা
  • ইন্টারপোলেটেড আউটপুট সরাসরি উচ্চ-রেটে স্যাম্পল করা প্রকৃত সিগন্যালের সাথে সংখ্যাগতভাবে যাচাই করা, এবং ফিল্টারের প্রান্তে (edge) কেন সামান্য বেশি ত্রুটি দেখা যায় তা বোঝা

১ · জিরো-ইনসার্শন — ফাঁকা জায়গা তৈরি করা

আপস্যাম্পলিংUpsamplingএকটি ডিসক্রিট সিগন্যালের স্যাম্পল সংখ্যা L গুণ বাড়ানোর প্রথম ধাপ, যেখানে প্রতিটি মূল স্যাম্পলের পর (L-1)টি শূন্য বসানো হয়। -এর প্রথম ধাপ হলো প্রতিটি স্যাম্পলের পর L-1টি শূন্য বসানো:

$$x_{\text{zero}}[n] = \begin{cases} x[n/L] & n \bmod L = 0 \\ 0 & \text{অন্যথায়} \end{cases}$$

এতে সিগন্যালের দৈর্ঘ্য L গুণ বেড়ে যায়, কিন্তু নতুন যোগ হওয়া মানগুলো নিছক শূন্য — কোনো অর্থবহ তথ্য নয়। এই জিরো-স্টাফড সিগন্যালকে "সরাসরি" উচ্চ রেটের সিগন্যাল হিসেবে ব্যবহার করলে ফলাফল হবে ঝাঁকুনিপূর্ণ (choppy) — মূল স্যাম্পলের মধ্যে হঠাৎ শূন্যে নেমে যাওয়া। দরকার একটি ফিল্টার যা এই শূন্যগুলোকে প্রতিবেশী স্যাম্পল থেকে আনুমানিক মসৃণ মান দিয়ে প্রতিস্থাপন করবে।

জিরো-ইনসার্শন
প্রতি মূল স্যাম্পলের পর L-1টি শূন্য — দৈর্ঘ্য L গুণ, কিন্তু কোনো নতুন তথ্য নেই।
লো-পাস ফিল্টার = ইন্টারপোলেটর
M7-এর একই windowed-sinc লো-পাস ফিল্টার এখানে শূন্যগুলো পূরণ করার কাজ করে — এটাই "ইন্টারপোলেশন ফিল্টার"।
গেইন সংশোধন ×L
জিরো-স্টাফিং গড় শক্তি কমিয়ে দেয় বলে ফিল্টারের গেইন L দিয়ে গুণ করে অ্যামপ্লিটিউড ঠিক রাখতে হয়।

২ · সত্যিকারের ডেমো — জিরো-স্টাফিং + লো-পাস ফিল্টার = ইন্টারপোলেশন

L40-এ আমরা একটি ৪৮ Hz সিগন্যালকে ১২ Hz-এ ডেসিমেট করেছিলাম। এবার ধরা যাক আমাদের কাছে শুধু একটি নিম্ন-রেট সিগন্যাল আছে — fs_low = 12 Hz-এ স্যাম্পল করা একটি ৩ Hz সাইন — আর আমরা এটিকে L = 4 দিয়ে আবার fs_new = 48 Hz-এ আপস্যাম্পল করতে চাই।

Python
import math

def sinc_lowpass(numtaps, fc):
    """windowed-sinc লো-পাস ফিল্টার (M7/L31-এর পদ্ধতি) -- fc = কাটঅফ, fs-এর ভগ্নাংশ হিসেবে (0..0.5)"""
    M = (numtaps - 1) / 2.0
    h = []
    for n in range(numtaps):
        d = n - M
        if abs(d) < 1e-9:
            hn = 2 * fc
        else:
            hn = math.sin(2 * math.pi * fc * d) / (math.pi * d)
        w = 0.54 - 0.46 * math.cos(2 * math.pi * n / (numtaps - 1))   # Hamming window
        h.append(hn * w)
    return h

def fir_apply(x, h):
    """ডিসক্রিট কনভোলিউশন (M3/L10) -- ফুল-লেংথ আউটপুট"""
    nx, nh = len(x), len(h)
    y = [0.0] * (nx + nh - 1)
    for n in range(nx):
        for k in range(nh):
            y[n + k] += x[n] * h[k]
    return y

fs_low = 12.0
f0 = 3.0
N_low = 12
x_low = [math.sin(2*math.pi*f0*n/fs_low) for n in range(N_low)]

L = 4
fs_new = fs_low * L    # ৪৮ Hz

# ধাপ ১ -- জিরো-ইনসার্শন
x_zeros = []
for v in x_low:
    x_zeros.append(v)
    x_zeros.extend([0.0] * (L - 1))

print(f"মূল সিগন্যাল ({fs_low} Hz, N={N_low}):", [round(v, 4) for v in x_low[:4]], "...")
print(f"জিরো-স্টাফড সিগন্যাল ({fs_new} Hz, N={len(x_zeros)}), প্রথম ১২টি মান:")
print([round(v, 4) for v in x_zeros[:12]])

# ধাপ ২ -- লো-পাস ইন্টারপোলেশন ফিল্টার, গেইন ×L দিয়ে স্কেল করা
numtaps = 25
fc = 0.5 / L                 # কাটঅফ = নতুন নাইকুইস্ট-এর ভগ্নাংশ হিসেবে মূল ব্যান্ডউইথ
h = sinc_lowpass(numtaps, fc)
h_scaled = [L * v for v in h]

y_full = fir_apply(x_zeros, h_scaled)
delay = (numtaps - 1) // 2
y_interp = y_full[delay: delay + len(x_zeros)]

# তুলনার জন্য -- একই সাইন সরাসরি ৪৮ Hz-এ স্যাম্পল করা "প্রকৃত" মান
x_true = [math.sin(2*math.pi*f0*n/fs_new) for n in range(len(x_zeros))]

print(f"\n{'n':>3} | {'zero-stuffed':>13} | {'interpolated':>13} | {'true (48Hz)':>12} | {'diff':>10}")
max_err = 0.0
for n in range(24):
    diff = abs(y_interp[n] - x_true[n])
    max_err = max(max_err, diff)
    print(f"{n:>3} | {x_zeros[n]:>13.4f} | {y_interp[n]:>13.4f} | {x_true[n]:>12.4f} | {diff:>10.2e}")

steady_state_err = max(abs(y_interp[n]-x_true[n]) for n in range(4, 24))
print(f"\nসর্বোচ্চ ত্রুটি (n=0..23, ফিল্টার-প্রান্ত সহ): {max_err:.4f}")
print(f"সর্বোচ্চ ত্রুটি (n=4..23, স্টেডি-স্টেট): {steady_state_err:.4f}")

    
জিরো-স্টাফড সিগন্যালে (n=0,1,2,3-এ মান 0, 0, 0, 0) কোনো তথ্য নেই — শুধু n=4-এ আসল মূল স্যাম্পল (1.0)। কিন্তু ফিল্টার করার পর interpolated কলামে n=1,2,3-এ যথাক্রমে ০.২৫২, ০.৫৭৯, ০.৮৬৫-এর মতো প্রকৃত, গণনা-করা মান দেখা যাচ্ছে — নিছক শূন্য নয়! n=0..3 এই প্রথম চক্রে ত্রুটি একটু বেশি (সর্বোচ্চ ০.১৩-এর কাছাকাছি) কারণ ফিল্টারের বাঁ পাশে কোনো "ইতিহাস" (আগের স্যাম্পল) নেই — একটি স্বাভাবিক ফিল্টার-প্রান্ত ট্রানজিয়েন্ট। কিন্তু n=4 থেকে শুরু করে স্টেডি-স্টেটে (যেখানে ফিল্টারের দুই পাশেই যথেষ্ট প্রতিবেশী স্যাম্পল আছে) ত্রুটি নেমে আসে মাত্র ০.০২-এর নিচে — অর্থাৎ ইন্টারপোলেটেড মান সরাসরি ৪৮ Hz-এ স্যাম্পল করা প্রকৃত মানের সাথে প্রায় হুবহু মিলে যায়।
মূল কথা · Key takeaway

ইন্টারপোলেশন মানে "ফাঁকা জায়গায় শূন্য বসিয়ে রাখা" নয় — এটি একটি দুই-ধাপের প্রক্রিয়া: প্রথমে জিরো-ইনসার্শন দিয়ে জায়গা তৈরি করা, তারপর M7-এর একই লো-পাস ফিল্টারকে (গেইন সংশোধন সহ) একটি "ইন্টারপোলেটর" হিসেবে ব্যবহার করে সেই জায়গা প্রতিবেশী স্যাম্পল থেকে আনুমানিক মসৃণ মান দিয়ে পূরণ করা। L40-এর ডেসিমেশন ফিল্টার আর এখানকার ইন্টারপোলেশন ফিল্টার — দুটোই মূলত একই লো-পাস FIR ধারণা, শুধু কাটঅফ আর গেইন ভিন্ন প্রেক্ষাপটে বসানো হয়েছে। L42-এ আমরা দেখব কীভাবে এই দুই অপারেশনকে (ফিল্টার + রেট পরিবর্তন) পলিফেজ স্ট্রাকচার দিয়ে আরও কার্যকরভাবে বাস্তবায়ন করা যায়।

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।

প্র ০১ উপরের কোডে ফিল্টারের গেইন L দিয়ে স্কেল না করলে (অর্থাৎ h_scaled = h ব্যবহার করলে) ইন্টারপোলেটেড আউটপুটের অ্যামপ্লিটিউডের কী হতো?

অ্যামপ্লিটিউড প্রায় L গুণ ছোট হয়ে যেত। জিরো-স্টাফিং প্রতি Lটি স্যাম্পলের মধ্যে মাত্র একটিতে প্রকৃত শক্তি রাখে (বাকি সব শূন্য), তাই ফিল্টার যদি "গড়" আচরণ করে (গেইন ১), আউটপুট মূল সিগন্যালের তুলনায় ম্লান হয়ে যাবে। গেইন ×L এই ক্ষয় পূরণ করে দেয় যাতে ইন্টারপোলেটেড সিগন্যালের অ্যামপ্লিটিউড মূল সিগন্যালের সাথে সামঞ্জস্যপূর্ণ থাকে।

প্র ০২ এই ইন্টারপোলেশন ফিল্টারের কাটঅফ fc = 0.5/L (নতুন রেটের ভগ্নাংশ হিসেবে) বেছে নেওয়া হয়েছে কেন — L40-এর অ্যান্টি-অ্যালিয়াস ফিল্টারের সাথে এর সম্পর্ক কী?

fc = 0.5/L মানে নতুন রেটে কাটঅফ fs_new × 0.5/L = fs_low/2 — অর্থাৎ ঠিক মূল, নিম্ন-রেট সিগন্যালের নাইকুইস্ট সীমায়। এর বাইরে যা কিছু ফ্রিকোয়েন্সি জিরো-স্টাফিং তৈরি করেছে (এগুলোকে "ইমেজ" বলা হয়, মূল স্পেকট্রামের পুনরাবৃত্তি) তা এই ফিল্টার সরিয়ে ফেলে। এটি L40-এর অ্যান্টি-অ্যালিয়াস ফিল্টারের ধারণাগত আয়না — সেখানে ফিল্টার ডেসিমেশনের আগে অবাঞ্ছিত উচ্চ-ফ্রিকোয়েন্সি শক্তি সরায়, এখানে ফিল্টার আপস্যাম্পলিং-এর পরে জিরো-ইনসার্শনের তৈরি করা অবাঞ্ছিত "ইমেজ" শক্তি সরায়।

প্র ০৩ n=0..3-এর প্রথম চক্রে ত্রুটি n=4 থেকে পরের চক্রগুলোর তুলনায় বেশি কেন — এটি কি একটি বাগ?

বাগ নয় — এটি একটি স্বাভাবিক FIR ফিল্টার-প্রান্ত (edge/transient) প্রভাব। ফিল্টারটি প্রতিটি আউটপুট গণনায় তার আশেপাশের ২৫টি ইনপুট স্যাম্পল ব্যবহার করে (২৫-ট্যাপ ফিল্টার), কিন্তু সিগন্যালের একদম শুরুতে (n=0..3) বাঁ দিকে যথেষ্ট "ইতিহাস" নেই — কোডে fir_apply ফাংশন সেই অনুপস্থিত মানগুলোকে implicitly শূন্য ধরে নেয়। n যত বাড়ে, ফিল্টার তত বেশি প্রকৃত প্রতিবেশী স্যাম্পল পায়, তাই ত্রুটি দ্রুত কমে যায় — এটি প্রতিটি বাস্তব FIR ফিল্টারিং অপারেশনের একটি সাধারণ, প্রত্যাশিত আচরণ (M7-এ বিস্তারিত)।

অনুশীলন

  1. চিন্তা করুন: উপরের কোডে L = 4-কে L = 2-এ বদলালে (fs_low=12 Hz অপরিবর্তিত রেখে) নতুন fs_new কত হবে, আর জিরো-স্টাফড সিগন্যালে প্রতি মূল স্যাম্পলের পর কতগুলো শূন্য বসবে?

    fs_new = fs_low × L = 12 × 2 = 24 Hz, এবং প্রতি মূল স্যাম্পলের পর L-1 = 1টি শূন্য বসবে (অর্থাৎ প্রতি দুটি আউটপুট স্যাম্পলের একটি মূল, একটি শূন্য-থেকে-ফিল্টার-করা)।

  2. পরীক্ষা করুন: কোডে L = 4-কে L = 2-এ পরিবর্তন করে (এবং fc = 0.5 / L স্বয়ংক্রিয়ভাবে আপডেট হবে) Run চেপে দেখুন স্টেডি-স্টেট ত্রুটি (n বড় হলে) আগের মতোই ছোট থাকে কি না।

    রান করলে দেখা যাবে স্টেডি-স্টেট ত্রুটি এখনও ছোট থাকে (একই ফিল্টার-ডিজাইন নীতি প্রযোজ্য, শুধু fc এখন 0.5/2 = 0.25), যদিও দৈর্ঘ্য আর ইন্টারপোলেটেড মানের সংখ্যা বদলে যাবে (মোট N_low × L = 24 স্যাম্পল, আগের ৪৮-এর বদলে) — যাচাই করুন প্রিন্ট হওয়া টেবিলের সংখ্যা দিয়ে।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

আগের পাঠ
ডাউনস্যাম্পলিং (ডেসিমেশন)