مقاله فنی

کرنل‌های downsample تصویر HotPDF و dithering چاپ

HotPDF سه کرنل downsample تصویر را از طریق property‏ ImageDownsampleKernel در دسترس می‌گذارد و یک گذر جداگانه Floyd-Steinberg را از طریق RenderOutputDither. اولی کنترل می‌کند عکس‌ها بعد از کوچک‌شدن برای خوردن در یک بودجه حجم چطور به نظر می‌رسند، دومی کنترل می‌کند بعد از کاهش صفحه به سیاه‌وسفید چطور دیده می‌شوند. هیچ‌کدام به‌صورت پیش‌فرض روشن نیست، و هر دو به همان یک دلیل opt-in‌اند: زمان واقعی هزینه دارند

فشاری که آدم‌ها را به اینجا می‌کشاند آشناست. یک قرارداد اسکن‌شده ۶۰ مگابایتی باید از یک گیت‌وی ایملی عبور کند که هر چیزی بالای ۱۰ مگابایت را رد می‌کند، یا یک دسته صورت‌حساب باید روی دستگاه تک‌رنگ از نوع فکس فرود بیاید که هر پیکسل خاکستری را یا کاغذ می‌بیند یا تونر. هر دو مسئله مسئله بازنمونه‌گیری‌اند، و هر دو هم یک جواب سریعِ بدشکل دارند هم یک جواب کندِ درست

سه کرنل واقعاً در چه چیزی فرق می‌کنند

THPDFResampleKernel سه مقدار دارد، و آن‌ها روی منحنی سرعت و کیفیت واقعاً در نقاط متفاوتی می‌نشینند. rkHalftone کار را به مسیر تاریخی GDI یعنی StretchBlt با حالت HALFTONE می‌سپارد، که با وجود اسمش فیلتری از رده bilinear است: سریع، برای line art و اسکرین‌شات کفایت می‌کند، و مستعد لبه‌های برشتگی‌ای است که روی عکس‌های کوچک‌شده فوراً می‌شناسید. rkBicubic یک کرنل جداشدنی Catmull-Rom اجرا می‌کند، و rkLanczos3 یک sinc پنجره‌دار جداشدنی با پشتیبانی سه‌لوبه

هر دو کرنل جداشدنی به‌صورت دو گذر اجرا می‌شوند، اول افقی بعد عمودی، با ۶ تا ۱۲ tap به‌ازای هر پیکسل مقصد در پاسکال خالص. این تقریباً یک مرتبه بزرگی کندتر از مسیر GDI است، که دقیقاً دلیلی است که rkHalftone پیش‌فرض می‌ماند. روی یک بچ شبانه چند هزار صفحه‌ای، تفاوت یک تصمیم زمان‌بندی است نه یک سلیقه. روی یک سند منفرد که کاربر منتظرش است، Lanczos3 تقریباً مجانی است و به‌وضوح بهتر

منحنی‌های وزن سه کرنل downsample در HotPDF: rkHalftone کار را به مسیر GDI با HALFTONE از رده bilinear با پشتیبانی یک می‌سپارد، rkBicubic یک درجه‌ای Catmull-Rom جداشدنی با پشتیبانی دو اجرا می‌کند، و rkLanczos3 یک sinc پنجره‌دار با پشتیبانی سه، که تقریباً یک مرتبه بزرگی از سرعت را در ازای عکس‌هایی به‌وضوح بهتر معامله می‌کند
سه مقدار کرنل واقعاً در نقاط متفاوتی روی منحنی سرعت و کیفیت می‌نشینند: یک مسیر GDI از رده bilinear، یک درجه‌ای Catmull-Rom و یک sinc پنجره‌دار سه‌لوبه، و کرنل‌های جداشدنی وزن‌ها را نرمال می‌کنند تا هیچ‌چیز از سیاه یا سفید فراتر نرود

دو property پیاده‌سازی ارزش می‌شنیدن دارند چون تعیین می‌کنند خروجی چه می‌تواند و چه نمی‌تواند. مرزها با تکرار لبه clamp می‌شوند نه wrap یا محو، و وزن‌ها به‌ازای هر پیکسل مقصد نرمال می‌شوند. این دو با هم یعنی نتیجه هرگز زیر سیاه یا بالای سفید ring نمی‌کند، پس هاله overshoot کلاسیک Lanczos دور یک لبه تیز به‌صورت artifact بریده‌شده در تصویر کدگذاری‌شده ظاهر نمی‌شود

var
  Pdf: THotPDF;
  Info: THPDFLoadedResourceOptimizationInfo;
  Changed: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.LoadFromFile('scanned-contract.pdf');
    Pdf.ImageDownsampleKernel := rkLanczos3;   // پیش از فراخوانی ست کنید
    Changed := Pdf.DownsampleLoadedImages(150, 82, 4096, Info);
    if Changed > 0 then
    begin
      Writeln('resampled images: ', Info.DownsampledImageCount);
      Writeln('kept calibrated : ', Info.PreservedCalibratedImageCount);
      Pdf.SaveToFile('scanned-contract-150dpi.pdf');
    end;
  finally
    Pdf.Free;
  end;
end;

آرگومان MinimumSavingsBytes، همان 4096 بالا، گاردی است که عملیات را صادق نگه می‌دارد. کدگذاری مجدد تصویری که از قبل به‌خوبی فشرده شده می‌تواند جریانی بزرگ‌تر از اصل تولید کند، و downsamplerای که کورکورانه هر تصویری را جایگزین می‌کند گاهی فایلی را که قرار بود کوچک کند بزرگ‌تر هم می‌کند. آستانه می‌گوید: جایگزینی را فقط وقتی commit کن که حداقل این‌قدر بایت صرفه‌جویی کند. PreservedCalibratedImageCount تصمیم محافظه‌کارانه دیگر را گزارش می‌کند، تصاویری که دست نخوردند چون فضای رنگ کالیبره‌شده‌ای حمل می‌کنند که بازنمونه‌گیری به خطر می‌اندازد

چرا یک ضریب چندجمله‌ای غلط این‌قدر سخت پیدا می‌شود؟

چون یک کرنل درون‌یابی خراب نه crash می‌کند نه exception می‌دهد، فقط تصویری تولید می‌کند که به شکلی ظریف غلط به نظر می‌رسد که هیچ‌کس نتواند منسوبش کند. کرنل Catmull-Rom درجه‌ایِ تکه‌ای است، و شاخه بیرونی‌اش در فرم تو در توی Horner چنین است: ((-0.5t + 2.5)t - 4)t + 2. آن ضریب میانی را به‌جای -4 بنویسید -5 و تابع هنوز مقدار می‌دهد، هنوز عددهایی در بازه باورپذیر برمی‌گرداند، و هنوز تصویری تولید می‌کند

آسیب به شکل این ظاهر می‌شود که W(1) به‌جای صفر، -1 می‌شود. وزن‌های منفی جمع می‌شوند، مجموع در صفر clip می‌شود، و علامت قابل مشاهده یک گرادیان است که سر چپش سیاه می‌شود و یک لبه پله‌ای که رنگ‌های میانی‌اش را از دست می‌دهد. هیچ‌چیز در این شکست به یک چندجمله‌ای اشاره نمی‌کند. چکی که در چند ثانیه شکارش می‌کند حسابی است نه بصری: یک کرنل درون‌یاب باید W(0) = 1 و W(±1) = W(±2) = 0 را برآورده کند، و هر کرنلی که این سه نقطه را از دست بدهد خطای ضریب دارد، تمام. روی همین سه مقدار در یونیت تست assert بگذارید و کل این رده عیب تایپی غیب می‌شود

نمودار شاخه بیرونی درجه‌ای Catmull-Rom در HotPDF که نشان می‌دهد چرا یک ضریب غلط قایم می‌شود: خطای تایپی که به‌جای -4 عدد -5 را در فرم تو در توی Horner می‌نویسد هنوز مقدار می‌دهد و W(1) را روی -1 و W(2) را روی -2 می‌گذارد جایی که صفر الزامی است، پس assert روی W(0) برابر ۱ به‌علاوه دو قید صفر آن را در چند ثانیه می‌گیرد
کرنل خراب هرگز crash نمی‌کند، فقط عددهایی برمی‌گرداند که باورپذیر به نظر می‌رسند، به همین دلیل چشم نمی‌تواند خطای تایپی ضریب را بگیرد. W(0) = 1 با صفرها در مثبت و منفی یک و دو، یک یونیت تست سه‌خطی است

Dithering به روش Floyd-Steinberg، و اینکه در خط لوله کجا می‌نشیند

گذر dither مسئله‌اش از بازنمونه‌گیری جداست و در نقطه دیگری از خط لوله زندگی می‌کند. RenderOutputDither پخش خطای Floyd-Steinberg را بعد از ترکیب صفحه اعمال می‌کند، که تنها جایی است که برای یک پیش‌نمایش چاپ تک‌رنگ یا یک خروجی از نوع فکس معنا دارد: این عملیات درباره کوچک‌کردن یک raster تمام‌شده به یک بیت به‌ازای هر پیکسل است، نه درباره اینکه تصاویر تکی در مسیر ورود چطور scale شده‌اند

خود الگوریتم کوتاه است. روشنایی در ۵۰ درصد آستانه می‌خورد، و خطای کوانتیزاسیون با وزن‌های کلاسیک 7/16 و 3/16 و 5/16 و 1/16 به چهار همسایه پخش می‌شود: راست، پایین-چپ، پایین و پایین-راست. پیکسل خروجی در هر کانال 0 یا 255 است. جایگزین ساده‌لوحانه به‌جایش به شما می‌دهد: آستانه سخت بدون هیچ پخشی، که یک عکس را به سیلوئت تبدیل می‌کند و هر رنگ میانی‌ای را که محتوا را حمل می‌کرد از دست می‌دهد

جای‌گیری در خط لوله رندر برای dithering به روش Floyd-Steinberg در HotPDF: RenderOutputDither بعد از ترکیب صفحه روی raster ۲۴بیتی تمام‌شده اجرا می‌شود، روشنایی را در ۵۰ درصد آستانه می‌کند و هر خطای کوانتیزاسیون را با وزن‌های 7/16 و 3/16 و 5/16 و 1/16 به راست و پایین از طریق یک بافر ردیفی که باید جمع کند پخش می‌سازد، و خروجی تک‌بیتی تک‌رنگ تولید می‌کند
Dithering بعد از ترکیب می‌نشیند چون یک raster تمام‌شده را به یک بیت می‌کاهد، نه به این دلیل که تصاویر چطور scale شده‌اند. وزن‌های پخش جمعشان یک می‌شود، و بافر ردیفی باید جمع کند نه بازنویسی
// dithering در زمان رندر برای یک دستگاه پیش‌نمایش تک‌رنگ
Pdf.RenderOutputDither := True;

// یا همان گذر را روی بیت‌مپی که از قبل دارید اعمال کنید. بیت‌مپ باید
// pf24bit باشد؛ تابع به‌جای حدس زدن False برمی‌گرداند
if not HPDFFloydSteinbergDitherBitmap(Preview) then
  raise Exception.Create('dither expects a 24-bit bitmap');

// دسترسی مستقیم به کرنل وقتی بیرون خط لوله سند بازنمونه‌گیری می‌کنید
Small := HPDFResizeBitmapKernel(Large, 640, 480, rkLanczos3);
try
  Small.SaveToFile('thumb.bmp');
finally
  Small.Free;
end;

یک جزئیات پیاده‌سازی در پخش خطا هست که یک بار سر هرکسی را می‌درد. بافر خطای ردیف به ردیف باید جمع کند. هر پیکسل در ردیف بعدی سهم‌هایی از سه پیکسل متفاوت در ردیف جاری می‌گیرد — tapهای 3/16 و 5/16 و 1/16 — و اگر کد به‌جای جمع‌کردن assign کند، هر نوشتن سهم قبلی را دور می‌ریزد و فقط آخرین tap زنده می‌ماند. تصویر هنوز dither شده به نظر می‌رسد، که همان چیزی است که دیدنش را سخت می‌کند، ولی بافت غلط است و بازتولید رنگی انحراف می‌گیرد. تستی که می‌گیردش کمی است: یک میدان خاکستری میانی یکنواخت را dither کنید و الزام کنید پوشش درونیه بین ۴۰ تا ۶۰ درصد فرود بیاید

خط لوله کاهش حجم کدام ترکیب را باید به کار ببرد؟

کرنل را با آنچه تصاویر واقعاً هستند جور کنید، و dithering را یک دغدغه دستگاه بدانید نه یک دغدغه فشرده‌سازی. برای اسکن‌های عکاسی که باید از بودجه حجم جان به در ببرند، rkLanczos3 در ۱۵۰ یا ۲۰۰ DPI جزئیاتی را که مردم می‌بینند نگه می‌دارد در حالی که شمار پیکسل را به اندازه چهار برابر یا بیشتر می‌کاهد. برای اسکرین‌شات، دیاگرام‌ها و line art، rkHalftone واقعاً کافی است و خیلی سریع‌تر، چون این تصاویر گرادیان رنگی کمی برای حفظ کردن دارند. برای یک بچ مخلوط که نمی‌توانید تک‌تک تصاویر را بازبینی کنید، rkBicubic میانه منطقی است: بهتر از bilinear، تقریباً نصف شمار tapهای Lanczos3

downsample یکی از چند اهرم است، و همیشه بزرگ‌ترینشان نیست. اسکن‌های دوسطحی معمولاً به انکودری که در فشرده‌سازی بومی JBIG2 دوسطحی در Delphi پوشش داده شد خیلی بهتر جواب می‌دهند، جایی که برد از دیکشنری‌های سمبل می‌آید نه از شمار پیکسل. پیش از تصمیم‌گرفتن، دانستن اینکه واقعاً چه چیزی در فایل هست کمک می‌کند، که کار استخراج تصاویر و فیلترهای decode آن‌ها همین است: یک فهرست از آبجکت‌های تصویر و فشرده‌سازی موجودشان به شما می‌گوید آیا بازنمونه‌گیری اصلاً چیزی برای برداشتن دارد

اگر دارید سطح پیش‌نمایشی می‌سازید که نتیجه را نشان می‌دهد، همان مسیر رندر مستندشده در رندر صفحه PDF به بیت‌مپ جایی است که RenderOutputDither اثر می‌کند، پس پیش‌نمایش dither شده و خروجی dither شده از یک مسیر کد می‌آیند نه از دو پیاده‌سازی که از هم فاصله بگیرند

اصل پشت هر دو قابلیت این است که تنظیمات کیفیت باید صریح و برگشت‌پذیر باشند. HotPDF رفتار تاریخی را پیش‌فرض نگه می‌دارد تا یک برنامه موجود بدون تغییر غافلگیرکننده در خروجی یا زمان‌بندی ارتقا پیدا کند، و مسیرهای خوش‌ظاهرتر و کندتر را به اندازه یک انتساب property دور نگه می‌دارد. هر دو بخشی از HotPDF Delphi PDF component هستند، کنار ماشین‌ری بهینه‌سازی resource و رندری که بر آن‌ها بنا شده‌اند