Zmień rozmiar obrazu za pomocą płótna javascript (płynnie)

90

Próbuję zmienić rozmiar niektórych obrazów za pomocą płótna, ale nie mam pojęcia, jak je wygładzić. W Photoshopie, przeglądarkach itp. Jest kilka algorytmów, których używają (np. Bicubic, bilinear), ale nie wiem, czy są one wbudowane w płótno, czy nie.

Oto moje skrzypce: http://jsfiddle.net/EWupT/

var canvas = document.createElement('canvas');
var ctx = canvas.getContext('2d');
canvas.width=300
canvas.height=234
ctx.drawImage(img, 0, 0, 300, 234);
document.body.appendChild(canvas);

Pierwszy to normalny tag obrazu o zmienionym rozmiarze, a drugi to płótno. Zwróć uwagę, że płótno nie jest tak gładkie. Jak mogę osiągnąć „gładkość”?

steve
źródło

Odpowiedzi:

136

Aby osiągnąć lepsze wyniki, możesz zastosować obniżanie poziomu. Większość przeglądarek wydaje się używać interpolacji liniowej zamiast dwuczęściowej podczas zmiany rozmiaru obrazów.

( Aktualizacja Do specyfikacji dodano właściwość jakości, imageSmoothingQualityktóra jest obecnie dostępna tylko w przeglądarce Chrome).

O ile nie wybierze się żadnego wygładzania lub najbliższego sąsiada, przeglądarka zawsze interpoluje obraz po zmniejszeniu go, ponieważ działa to jako filtr dolnoprzepustowy, aby uniknąć aliasingu.

Bi-linear wykorzystuje 2x2 piksele do wykonania interpolacji, podczas gdy bi-sześcienny wykorzystuje 4x4, więc wykonując to krokowo, można zbliżyć się do wyniku dwu-sześciennego, używając interpolacji bi-liniowej, jak widać na wynikowych obrazach.

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

img.onload = function () {

    // set size proportional to image
    canvas.height = canvas.width * (img.height / img.width);

    // step 1 - resize to 50%
    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);

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

    // step 3, resize to final size
    ctx.drawImage(oc, 0, 0, oc.width * 0.5, oc.height * 0.5,
    0, 0, canvas.width, canvas.height);
}
img.src = "//i.imgur.com/SHo6Fub.jpg";
<img src="//i.imgur.com/SHo6Fub.jpg" width="300" height="234">
<canvas id="canvas" width=300></canvas>

W zależności od tego, jak drastyczna jest zmiana rozmiaru, możesz pominąć krok 2, jeśli różnica jest mniejsza.

W wersji demonstracyjnej możesz zobaczyć, że nowy wynik jest teraz bardzo podobny do elementu obrazu.


źródło
1
@steve heh, czasami takie rzeczy się zdarzają :) W przypadku obrazów można to zwykle nadpisać, ustawiając css BTW.
Ken, pierwszy wynik zadziałał świetnie, ale kiedy zmieniam zdjęcia, widać, że są zbyt rozmyte. Jsfiddle.net/kcHLG Co można zrobić w tym i innych przypadkach?
steve
@steve możesz zmniejszyć liczbę kroków do 1 lub żadnego (w przypadku niektórych obrazów działa to dobrze). Zobacz również tę odpowiedź, która jest podobna do tej, ale tutaj dodałem do niej wyostrzoną konwolucję, dzięki czemu można wyostrzyć wynikowy obraz po jego zmniejszeniu.
1
@steve tutaj to zmodyfikowane skrzypce z Billem, używając tylko jednego dodatkowego kroku: jsfiddle.net/AbdiasSoftware/kcHLG/1
1
@neaumusic kod jest kontynuacją kodu PO. Jeśli otworzysz skrzypce, zobaczysz definiowanie ctx. Podałem to tutaj, aby uniknąć nieporozumień.
27

Ponieważ skrzypce Trung Le Nguyen Nhat w ogóle nie są poprawne (w ostatnim kroku używa po prostu oryginalnego obrazu)
, napisałem własne ogólne skrzypce z porównaniem wydajności:

SKRZYPCE

Zasadniczo jest to:

img.onload = function() {
   var canvas = document.createElement('canvas'),
       ctx = canvas.getContext("2d"),
       oc = document.createElement('canvas'),
       octx = oc.getContext('2d');

   canvas.width = width; // destination canvas size
   canvas.height = canvas.width * img.height / img.width;

   var cur = {
     width: Math.floor(img.width * 0.5),
     height: Math.floor(img.height * 0.5)
   }

   oc.width = cur.width;
   oc.height = cur.height;

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

   while (cur.width * 0.5 > width) {
     cur = {
       width: Math.floor(cur.width * 0.5),
       height: Math.floor(cur.height * 0.5)
     };
     octx.drawImage(oc, 0, 0, cur.width * 2, cur.height * 2, 0, 0, cur.width, cur.height);
   }

   ctx.drawImage(oc, 0, 0, cur.width, cur.height, 0, 0, canvas.width, canvas.height);
}
Sebastian Ott
źródło
Najbardziej niedoceniana odpowiedź, jaką kiedykolwiek widziałem.
Amsakanna
17

Stworzyłem usługę Angular wielokrotnego użytku do obsługi wysokiej jakości zmiany rozmiaru obrazów / płócien dla każdego, kto jest zainteresowany: https://gist.github.com/transitive-bullshit/37bac5e741eaec60e983

Usługa obejmuje dwa rozwiązania, ponieważ oba mają swoje wady / zalety. Podejście splotu lanczosa zapewnia wyższą jakość kosztem wolniejszego, podczas gdy podejście stopniowego zmniejszania skali daje rozsądnie antyaliasowane wyniki i jest znacznie szybsze.

Przykładowe użycie:

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
    })
})
fisch2
źródło
Przepraszam za to - zmieniłem nazwę użytkownika na githubie. Wystarczy uaktualnił łącza gist.github.com/transitive-bullshit/37bac5e741eaec60e983
fisch2
3
Widziałem słowo kanciasty, poczułem się zabawnie
SuperUberDuper
8

Chociaż niektóre z tych fragmentów kodu są krótkie i działają, nie są one łatwe do naśladowania i zrozumienia.

Ponieważ nie jestem fanem „kopiuj-wklej” z przepełnienia stosu, chciałbym, aby programiści zrozumieli kod, który umieszczają w swoim oprogramowaniu. Mam nadzieję, że poniższe informacje okażą się przydatne.

DEMO : Zmiana rozmiaru obrazów za pomocą skrzynek JS i HTML Canvas Demo.

Możesz znaleźć 3 różne metody zmiany rozmiaru, które pomogą ci zrozumieć, jak działa kod i dlaczego.

https://jsfiddle.net/1b68eLdr/93089/

Pełny kod zarówno demonstracji, jak i metody TypeScript, której możesz chcieć użyć w swoim kodzie, można znaleźć w projekcie GitHub.

https://github.com/eyalc4/ts-image-resizer

To jest ostateczny kod:

export class ImageTools {
base64ResizedImage: string = null;

constructor() {
}

ResizeImage(base64image: string, width: number = 1080, height: number = 1080) {
    let img = new Image();
    img.src = base64image;

    img.onload = () => {

        // Check if the image require resize at all
        if(img.height <= height && img.width <= width) {
            this.base64ResizedImage = base64image;

            // TODO: Call method to do something with the resize image
        }
        else {
            // Make sure the width and height preserve the original aspect ratio and adjust if needed
            if(img.height > img.width) {
                width = Math.floor(height * (img.width / img.height));
            }
            else {
                height = Math.floor(width * (img.height / img.width));
            }

            let resizingCanvas: HTMLCanvasElement = document.createElement('canvas');
            let resizingCanvasContext = resizingCanvas.getContext("2d");

            // Start with original image size
            resizingCanvas.width = img.width;
            resizingCanvas.height = img.height;


            // Draw the original image on the (temp) resizing canvas
            resizingCanvasContext.drawImage(img, 0, 0, resizingCanvas.width, resizingCanvas.height);

            let curImageDimensions = {
                width: Math.floor(img.width),
                height: Math.floor(img.height)
            };

            let halfImageDimensions = {
                width: null,
                height: null
            };

            // Quickly reduce the dize by 50% each time in few iterations until the size is less then
            // 2x time the target size - the motivation for it, is to reduce the aliasing that would have been
            // created with direct reduction of very big image to small image
            while (curImageDimensions.width * 0.5 > width) {
                // Reduce the resizing canvas by half and refresh the image
                halfImageDimensions.width = Math.floor(curImageDimensions.width * 0.5);
                halfImageDimensions.height = Math.floor(curImageDimensions.height * 0.5);

                resizingCanvasContext.drawImage(resizingCanvas, 0, 0, curImageDimensions.width, curImageDimensions.height,
                    0, 0, halfImageDimensions.width, halfImageDimensions.height);

                curImageDimensions.width = halfImageDimensions.width;
                curImageDimensions.height = halfImageDimensions.height;
            }

            // Now do final resize for the resizingCanvas to meet the dimension requirments
            // directly to the output canvas, that will output the final image
            let outputCanvas: HTMLCanvasElement = document.createElement('canvas');
            let outputCanvasContext = outputCanvas.getContext("2d");

            outputCanvas.width = width;
            outputCanvas.height = height;

            outputCanvasContext.drawImage(resizingCanvas, 0, 0, curImageDimensions.width, curImageDimensions.height,
                0, 0, width, height);

            // output the canvas pixels as an image. params: format, quality
            this.base64ResizedImage = outputCanvas.toDataURL('image/jpeg', 0.85);

            // TODO: Call method to do something with the resize image
        }
    };
}}
Eyal c
źródło
4

Stworzyłem bibliotekę, która pozwala obniżyć dowolny procent, zachowując wszystkie dane dotyczące kolorów.

https://github.com/danschumann/limby-resize/blob/master/lib/canvas_resize.js

Ten plik, który możesz dołączyć do przeglądarki. Wyniki będą wyglądać jak Photoshop lub Magia Obrazów, zachowując wszystkie dane kolorów, uśredniając piksele, zamiast pobierać pobliskie i upuszczać inne. Nie używa wzoru do odgadywania średnich, bierze dokładną średnią.

Funkodebat
źródło
1
Prawdopodobnie użyłbym teraz webgl do zmiany rozmiaru
Funkodebat
4

Na podstawie odpowiedzi K3N przepisuję kod ogólnie dla każdego

var oc = document.createElement('canvas'), octx = oc.getContext('2d');
    oc.width = img.width;
    oc.height = img.height;
    octx.drawImage(img, 0, 0);
    while (oc.width * 0.5 > width) {
       oc.width *= 0.5;
       oc.height *= 0.5;
       octx.drawImage(oc, 0, 0, oc.width, oc.height);
    }
    oc.width = width;
    oc.height = oc.width * img.height / img.width;
    octx.drawImage(img, 0, 0, oc.width, oc.height);

ZAKTUALIZUJ DEMO JSFIDDLE

Oto moja DEMO ONLINE

Trung Le Nguyen Nhat
źródło
2
To nie zadziała: za każdym razem, gdy zmienisz rozmiar płótna, wyczyści to jego kontekst. Potrzebujesz 2 płótna. Tutaj jest tak samo, jak bezpośrednie wywołanie funkcji drawImage z ostatecznymi wymiarami.
Kaiido,
2

Nie rozumiem, dlaczego nikt nie sugeruje createImageBitmap.

createImageBitmap(
    document.getElementById('image'), 
    { resizeWidth: 300, resizeHeight: 234, resizeQuality: 'high' }
)
.then(imageBitmap => 
    document.getElementById('canvas').getContext('2d').drawImage(imageBitmap, 0, 0)
);

działa pięknie (zakładając, że ustawisz identyfikatory dla obrazu i płótna).

cagdas_ucar
źródło
Ponieważ nie jest szeroko obsługiwany caniuse.com/#search=createImageBitmap
Matt
createImageBitmap jest obsługiwana przez 73% wszystkich użytkowników. W zależności od przypadku użycia może być wystarczająco dobry. To tylko Safari odmawia dodania dla niego wsparcia. Myślę, że warto wspomnieć o możliwym rozwiązaniu.
cagdas_ucar
Niezłe rozwiązanie, ale niestety nie działa na
Firefoksie
1

Napisałem małe narzędzie js do przycinania i zmiany rozmiaru obrazu na interfejsie użytkownika. Oto link do projektu GitHub. Możesz także pobrać blob z końcowego obrazu, aby go wysłać.

import imageSqResizer from './image-square-resizer.js'

let resizer = new imageSqResizer(
    'image-input',
    300,
    (dataUrl) => 
        document.getElementById('image-output').src = dataUrl;
);
//Get blob
let formData = new FormData();
formData.append('files[0]', resizer.blob);

//get dataUrl
document.getElementById('image-output').src = resizer.dataUrl;
Diyaz Yakubov
źródło