Html5 canvas drawImage: Cách áp dụng khử răng cưa


81

Hãy xem ví dụ sau:

http://jsfiddle.net/MLGr4/47/

var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");

img = new Image();
img.onload = function(){
    canvas.width = 400;
    canvas.height = 150;
    ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, 400, 150);
}
img.src = "http://openwalls.com/image/1734/colored_lines_on_blue_background_1920x1200.jpg";

Như bạn thấy, hình ảnh không được khử răng cưa mặc dù người ta nói rằng drawImage áp dụng khử răng cưa tự động. Tôi đã thử nhiều cách khác nhau nhưng dường như không hiệu quả. Bạn có thể vui lòng cho tôi biết làm thế nào tôi có thể có được hình ảnh chống răng cưa? Cảm ơn.

Câu trả lời:


174

Nguyên nhân

Một số hình ảnh rất khó lấy mẫu và nội suy, chẳng hạn như hình này có các đường cong khi bạn muốn chuyển từ kích thước lớn sang kích thước nhỏ.

Các trình duyệt dường như thường sử dụng phép nội suy hai tuyến tính (lấy mẫu 2x2) với phần tử canvas thay vì hai khối (lấy mẫu 4x4) vì lý do hiệu suất (có thể xảy ra).

Nếu bước quá lớn thì đơn giản là không có đủ pixel để lấy mẫu mà từ đó được phản ánh trong kết quả.

Từ góc độ tín hiệu / DSP, bạn có thể xem đây là giá trị ngưỡng của bộ lọc thông thấp được đặt quá cao, điều này có thể dẫn đến hiện tượng răng cưa nếu có nhiều tần số cao (chi tiết) trong tín hiệu.

Giải pháp

Cập nhật 2018:

Đây là một mẹo nhỏ mà bạn có thể sử dụng cho các trình duyệt hỗ trợ thuộc filtertính trên bối cảnh 2D. Điều này làm mờ trước hình ảnh về bản chất giống như lấy mẫu lại, sau đó thu nhỏ lại. Điều này cho phép các bước lớn nhưng chỉ cần hai bước và hai lần rút.

Làm mờ trước bằng cách sử dụng số bước (kích thước ban đầu / kích thước đích / 2) làm bán kính (bạn có thể cần điều chỉnh điều này theo kinh nghiệm dựa trên trình duyệt và các bước chẵn / lẻ - ở đây chỉ hiển thị đơn giản):

const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");

if (typeof ctx.filter === "undefined") {
 alert("Sorry, the browser doesn't support Context2D filters.")
}

const img = new Image;
img.onload = function() {

  // step 1
  const oc = document.createElement('canvas');
  const octx = oc.getContext('2d');
  oc.width = this.width;
  oc.height = this.height;

  // steo 2: pre-filter image using steps as radius
  const steps = (oc.width / canvas.width)>>1;
  octx.filter = `blur(${steps}px)`;
  octx.drawImage(this, 0, 0);

  // step 3, draw scaled
  ctx.drawImage(oc, 0, 0, oc.width, oc.height, 0, 0, canvas.width, canvas.height);

}
img.src = "//i.stack.imgur.com/cYfuM.jpg";
body{ background-color: ivory; }
canvas{border:1px solid red;}
<br/><p>Original was 1600x1200, reduced to 400x300 canvas</p><br/>
<canvas id="canvas" width=400 height=250></canvas>

Hỗ trợ cho bộ lọc như ogf tháng 10 năm 2018:

Cập nhật 2017: Hiện có một thuộc tính mới được xác định trong thông số kỹ thuật để đặt chất lượng lấy mẫu lại:

context.imageSmoothingQuality = "low|medium|high"

Nó hiện chỉ được hỗ trợ trong Chrome. Các phương pháp thực tế được sử dụng cho mỗi cấp độ là do nhà cung cấp quyết định, nhưng sẽ hợp lý khi giả định Lanczos cho "cao" hoặc một cái gì đó tương đương về chất lượng. Điều này có nghĩa là bước xuống có thể bị bỏ qua hoàn toàn hoặc các bước lớn hơn có thể được sử dụng với ít bản vẽ lại hơn, tùy thuộc vào kích thước hình ảnh và

Hỗ trợ cho imageSmoothingQuality:

trình duyệt. Cho đến khi đó ..:
Kết thúc truyền

Giải pháp là sử dụng bước xuống để có được kết quả thích hợp. Bước xuống có nghĩa là bạn giảm kích thước theo từng bước để cho phép phạm vi nội suy giới hạn bao phủ đủ pixel để lấy mẫu.

Điều này sẽ cho phép kết quả tốt cũng với nội suy hai tuyến tính (nó thực sự hoạt động giống như hai khối khi làm điều này) và chi phí là tối thiểu vì có ít pixel hơn để lấy mẫu trong mỗi bước.

Bước lý tưởng là đi đến một nửa độ phân giải trong mỗi bước cho đến khi bạn đặt được kích thước mục tiêu (cảm ơn Joe Mabel đã đề cập đến vấn đề này!).

Fiddle sửa đổi

Sử dụng quy mô trực tiếp như trong câu hỏi ban đầu:

HÌNH ẢNH ĐƯỢC KÉO XUỐNG BÌNH THƯỜNG

Sử dụng bước xuống như hình dưới đây:

HÌNH ẢNH ĐƯỢC BƯỚC XUỐNG

Trong trường hợp này, bạn cần phải từ bỏ trong 3 bước:

Trong bước 1, chúng tôi giảm hình ảnh xuống một nửa bằng cách sử dụng canvas ngoài màn hình:

// step 1 - create off-screen canvas
var oc   = document.createElement('canvas'),
    octx = oc.getContext('2d');

oc.width  = img.width  * 0.5;
oc.height = img.height * 0.5;

octx.drawImage(img, 0, 0, oc.width, oc.height);

Bước 2 sử dụng lại canvas ngoài màn hình và lại thu nhỏ hình ảnh xuống một nửa:

// step 2
octx.drawImage(oc, 0, 0, oc.width * 0.5, oc.height * 0.5);

Và chúng tôi vẽ một lần nữa vào canvas chính, một lần nữa giảm xuống một nửa nhưng đến kích thước cuối cùng:

// step 3
ctx.drawImage(oc, 0, 0, oc.width * 0.5, oc.height * 0.5,
                  0, 0, canvas.width,   canvas.height);

Tiền boa:

Bạn có thể tính toán tổng số bước cần thiết, sử dụng công thức này (nó bao gồm bước cuối cùng để đặt kích thước mục tiêu):

steps = Math.ceil(Math.log(sourceWidth / targetWidth) / Math.log(2))

4
Làm việc với một số hình ảnh ban đầu rất lớn (8000 x 6000 trở lên), tôi thấy hữu ích khi lặp lại bước 2 về cơ bản cho đến khi tôi đạt được kích thước bằng hệ số 2 của kích thước mong muốn.
Joe Mabel

Hoạt động như một sự quyến rũ! Thanx!
Vlad Tsepelev

1
Tôi nhầm lẫn về sự khác biệt giữa bước thứ 2 và bước thứ 3 ... bất cứ ai có thể giải thích?
carinlynchin

1
@Carine thì hơi phức tạp, nhưng canvas cố gắng lưu png nhanh nhất có thể. Tệp png hỗ trợ 5 loại bộ lọc khác nhau bên trong có thể cải thiện khi nén (gzip), nhưng để tìm ra sự kết hợp tốt nhất, tất cả các bộ lọc này phải được kiểm tra trên mỗi dòng của hình ảnh. Điều đó sẽ tốn thời gian cho các hình ảnh lớn và có thể chặn trình duyệt, vì vậy hầu hết các trình duyệt chỉ sử dụng bộ lọc 0 và đẩy nó ra với hy vọng đạt được một số nén. Bạn có thể thực hiện quá trình này theo cách thủ công nhưng rõ ràng là nó sẽ làm việc nhiều hơn một chút. Hoặc chạy nó thông qua các API dịch vụ chẳng hạn như của tinypng.com.

1
@Kaiido nó không bị quên và "sao chép" rất chậm. Bạn cần tính minh bạch, nhanh hơn là sử dụng clearRect () và sử dụng main hoặc alt. canvas làm mục tiêu.

12

Tôi rất khuyên bạn nên pica cho những nhiệm vụ như vậy. Chất lượng của nó vượt trội hơn so với nhiều lần giảm kích thước và khá nhanh cùng một lúc. Đây là một bản demo .


4
    var getBase64Image = function(img, quality) {
    var canvas = document.createElement("canvas");
    canvas.width = img.width;
    canvas.height = img.height;
    var ctx = canvas.getContext("2d");

    //----- origin draw ---
    ctx.drawImage(img, 0, 0, img.width, img.height);

    //------ reduced draw ---
    var canvas2 = document.createElement("canvas");
    canvas2.width = img.width * quality;
    canvas2.height = img.height * quality;
    var ctx2 = canvas2.getContext("2d");
    ctx2.drawImage(canvas, 0, 0, img.width * quality, img.height * quality);

    // -- back from reduced draw ---
    ctx.drawImage(canvas2, 0, 0, img.width, img.height);

    var dataURL = canvas.toDataURL("image/png");
    return dataURL;
    // return dataURL.replace(/^data:image\/(png|jpg);base64,/, "");
}

1
phạm vi giá trị của tham số 'chất lượng' là gì?
serup

đặt cược giữa số 0 và một [0, 1]
Iván Rodríguez,

4

Ngoài câu trả lời của Ken, đây là một giải pháp khác để thực hiện việc lấy mẫu xuống thành một nửa (vì vậy kết quả có vẻ tốt khi sử dụng thuật toán của trình duyệt):

  function resize_image( src, dst, type, quality ) {
     var tmp = new Image(),
         canvas, context, cW, cH;

     type = type || 'image/jpeg';
     quality = quality || 0.92;

     cW = src.naturalWidth;
     cH = src.naturalHeight;

     tmp.src = src.src;
     tmp.onload = function() {

        canvas = document.createElement( 'canvas' );

        cW /= 2;
        cH /= 2;

        if ( cW < src.width ) cW = src.width;
        if ( cH < src.height ) cH = src.height;

        canvas.width = cW;
        canvas.height = cH;
        context = canvas.getContext( '2d' );
        context.drawImage( tmp, 0, 0, cW, cH );

        dst.src = canvas.toDataURL( type, quality );

        if ( cW <= src.width || cH <= src.height )
           return;

        tmp.src = dst.src;
     }

  }
  // The images sent as parameters can be in the DOM or be image objects
  resize_image( $( '#original' )[0], $( '#smaller' )[0] );

3

Trong trường hợp người khác vẫn tìm kiếm câu trả lời, có một cách khác mà bạn có thể sử dụng hình nền thay vì drawImage (). Bạn sẽ không mất bất kỳ chất lượng hình ảnh nào theo cách này.

JS:

    var canvas=document.getElementById("canvas");
    var ctx=canvas.getContext("2d");
   var url = "http://openwalls.com/image/17342/colored_lines_on_blue_background_1920x1200.jpg";

    img=new Image();
    img.onload=function(){

        canvas.style.backgroundImage = "url(\'" + url + "\')"

    }
    img.src="http://openwalls.com/image/17342/colored_lines_on_blue_background_1920x1200.jpg";

bản demo làm việc


2

Tôi đã tạo một dịch vụ Angular có thể tái sử dụng để xử lý thay đổi kích thước hình ảnh chất lượng cao cho bất kỳ ai quan tâm: https://gist.github.com/fisch0920/37bac5e741eaec60e983

Dịch vụ này bao gồm cách tiếp cận giảm tỷ lệ từng bước của Ken cũng như phiên bản sửa đổi của phương pháp tích chập lanczos được tìm thấy tại đây .

Tôi bao gồm cả hai giải pháp vì cả hai đều có ưu / nhược điểm riêng. Phương pháp tích chập lanczos có chất lượng cao hơn với chi phí là chậm hơn, trong khi phương pháp giảm tỉ lệ từng bước tạo ra kết quả chống răng cưa hợp lý và nhanh hơn đáng kể.

Ví dụ sử dụng:

angular.module('demo').controller('ExampleCtrl', function (imageService) {
  // EXAMPLE USAGE
  // NOTE: it's bad practice to access the DOM inside a controller, 
  // but this is just to show the example usage.

  // resize by lanczos-sinc filter
  imageService.resize($('#myimg')[0], 256, 256)
    .then(function (resizedImage) {
      // do something with resized image
    })

  // resize by stepping down image size in increments of 2x
  imageService.resizeStep($('#myimg')[0], 256, 256)
    .then(function (resizedImage) {
      // do something with resized image
    })
})
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.