การจัดการกับรูปภาพขนาดใหญ่ในผ้าใบ HTML5

ฉันมีภาพเวกเตอร์ขนาด 15,000x15000 px ที่ฉันต้องการใช้เป็นพื้นหลังสำหรับโปรเจ็กต์ Canvas ฉันต้องสามารถตัดส่วนของภาพและวาดเป็นพื้นหลังได้อย่างรวดเร็วและบ่อยครั้ง (ภายใน requestAnimationFrame)

ในการวาดส่วนที่ต้องการของภาพที่ฉันใช้...

const xOffset = (pos.x - (canvas.width / 2)) + config.bgOffset + config.arenaRadius;
const yOffset = (pos.y - (canvas.height / 2)) + config.bgOffset + config.arenaRadius;
c.drawImage(image, xOffset, yOffset, canvas.width, canvas.height, 0, 0, canvas.width, canvas.height);

เพื่อคำนวณพื้นที่พื้นหลังที่ต้องการและวาดภาพ ทั้งหมดนี้ทำงานได้ดี แต่การวาดใหม่ช้าและการดึงครั้งแรกจะช้ากว่าด้วยซ้ำ

การโหลดภาพขนาดใหญ่นี้ดูไร้สาระและประสิทธิภาพการทำงานก็เชื่องช้า ฉันจะลดขนาดไฟล์หรือปรับปรุงประสิทธิภาพได้อย่างไร

แก้ไข: ไม่มีวิธีแก้ปัญหาที่สมเหตุสมผลในการวาดภาพขนาดใหญ่ในขนาดเต็ม เนื่องจากพื้นหลังเป็นรูปแบบที่ซ้ำกัน วิธีแก้ปัญหาปัจจุบันของฉันคือใช้ "เซลล์" รูปแบบเดียวแล้ววาดหลายครั้ง


person jolaleye    schedule 18.05.2018    source แหล่งที่มา
comment
ผ้าใบของคุณใหญ่แค่ไหน? นอกจากนี้ภาพเวกเตอร์ใน SVG?   -  person Máté Safranka    schedule 19.05.2018
comment
@ MátéSafranka ผืนผ้าใบปรับขนาดตามขนาดหน้าจอ ขนาดดั้งเดิมที่ฉันออกแบบคือ 1920x1080 (1920x974 สำหรับหน้าต่างจริง) ฉันสร้างพื้นหลังใน Affinity Designer เพื่อให้สามารถส่งออกเป็น SVG ได้   -  person jolaleye    schedule 19.05.2018
comment
ฉันกำลังออกไปนอกเขตความมั่นใจของตัวเองเล็กน้อย แต่นี่คือสองเซ็นต์ของฉัน การวาดบิตแมปเสี้ยวหนึ่งไม่ควรเป็นการดำเนินการที่ต้องใช้ CPU มากเป็นพิเศษ เนื่องจากโดยพื้นฐานแล้วเป็นเพียงการคัดลอกไบต์จำนวนหนึ่งจากบัฟเฟอร์หนึ่งไปยังอีกบัฟเฟอร์หนึ่ง แหล่งที่มาเดียวของการชะลอตัวที่ฉันจินตนาการได้ว่ามีจำนวนไบต์ที่จะคัดลอก เช่น ขนาดของผืนผ้าใบ เป้าหมาย อย่างไรก็ตาม - และนี่คือจุดที่ความมั่นใจของฉันลดลง - การวาดภาพจาก SVG อาจเป็นอย่างอื่น เนื่องจากในกรณีนี้แหล่งที่มาไม่ใช่บิตแมป ซึ่งหมายความว่าต้องมีการคำนวณเพิ่มเติมมากมาย   -  person Máté Safranka    schedule 19.05.2018
comment
ดังนั้น นอกเหนือจากการใช้ผืนผ้าใบขนาดเล็ก ฉันเดาอย่างเดียวคือใช้บิตแมปแทน SVG เป็นแหล่งที่มา อย่างไรก็ตาม ที่ขนาด 15,000 x 15,000 พิกเซล นั่นอาจจะกินหน่วยความจำมากเกินไป   -  person Máté Safranka    schedule 19.05.2018
comment
@ MátéSafranka ฉันจะยุ่งกับมันอีกสักหน่อยเพื่อดูว่ามันช้าแค่ไหนในขณะที่ฉันดำเนินการกับโครงการ ขนาดไฟล์ภาพคือ 9.23MB... ฉันเดาว่านั่นทำให้ธงสีแดงบางอย่างขึ้น สำหรับประเด็นสุดท้ายของคุณ รูปภาพได้รับการออกแบบด้วยโปรแกรมเวกเตอร์ แต่สามารถส่งออกเป็น PNG ได้ (ซึ่งเป็นสิ่งที่ฉันใช้) อันที่จริง เมื่อฉันพยายามครั้งแรก การวาด SVG ไม่ได้ผลด้วยซ้ำ   -  person jolaleye    schedule 19.05.2018


คำตอบ (2)


15,000px x 15,000px นั้นใหญ่มาก

GPU จะต้องจัดเก็บมันเป็นข้อมูล RGB แบบดิบในหน่วยความจำ (ฉันจำคณิตศาสตร์ไม่ได้แน่ชัด แต่ฉันคิดว่ามันมีขนาดประมาณ กว้าง x สูง x 3 ไบต์ เช่น 675MB ในกรณีของคุณ ซึ่งมากกว่าที่ GPU ทั่วไปสามารถทำได้ จัดการ).
เพิ่มกราฟิกอื่นๆ ทั้งหมดที่คุณมี และ GPU ของคุณจะถูกบังคับให้วางภาพใหญ่ของคุณและดึงมันอีกครั้งทุกเฟรม

เพื่อหลีกเลี่ยงปัญหานี้ คุณควรแบ่งภาพใหญ่ออกเป็นภาพเล็กๆ หลายภาพ และเรียกหลายครั้ง drawImage ต่อเฟรม ด้วยวิธีนี้ ในกรณีที่เลวร้ายที่สุด GPU จะต้องดึงเฉพาะชิ้นส่วนที่จำเป็นเท่านั้น และในกรณีที่ดีที่สุด GPU ก็จะมีชิ้นส่วนนั้นอยู่ในหน่วยความจำอยู่แล้ว

นี่คือข้อพิสูจน์แนวคิดคร่าวๆ ซึ่งจะแบ่งรูปภาพ SVG ขนาด 5,000*5000px ออกเป็นไทล์ขนาด 250*250px แน่นอนว่าคุณจะต้องปรับให้เข้ากับความต้องการของคุณ แต่มันอาจจะทำให้คุณมีไอเดียได้

console.log('generating image...');
var bigImg = new Image();
bigImg.src = URL.createObjectURL(generateBigImage(5000, 5000));
bigImg.onload = init;

function splitBigImage(img, maxSize) {
  if (!maxSize || typeof maxSize !== 'number') maxSize = 500;

  var iw = img.naturalWidth,
    ih = img.naturalHeight,
    tw = Math.min(maxSize, iw),
    th = Math.min(maxSize, ih),
    tileCols = Math.ceil(iw / tw), // how many columns we'll have
    tileRows = Math.ceil(ih / th), // how many rows we'll have
    tiles = [],
    r, c, canvas;

  // draw every part of our image once on different canvases
  for (r = 0; r < tileRows; r++) {
    for (c = 0; c < tileCols; c++) {
      canvas = document.createElement('canvas');
      // add a 1px margin all around for antialiasing when drawing at non integer
      canvas.width = tw + 2;
      canvas.height = th + 2;
      canvas.getContext('2d')
        .drawImage(img,
          (c * tw | 0) - 1, // compensate the 1px margin
          (r * tw | 0) - 1,
          iw, ih, 0, 0, iw, ih);
      tiles.push(canvas);
    }
  }

  return {
    width: iw,
    height: ih,
    // the drawing function, takes the output context and x,y positions
    draw: function drawBackground(ctx, x, y) {
      var cw = ctx.canvas.width,
        ch = ctx.canvas.height;
      // get our visible rectangle as rows and columns indexes
      var firstRowIndex = Math.max(Math.floor((y - th) / th), 0),
        lastRowIndex = Math.min(Math.ceil((ch + y) / th), tileRows),
        firstColIndex = Math.max(Math.floor((x - tw) / tw), 0),
        lastColIndex = Math.min(Math.ceil((cw + x) / tw), tileCols);

      var col, row;
      // loop through visible tiles and draw them
      for (row = firstRowIndex; row < lastRowIndex; row++) {
        for (col = firstColIndex; col < lastColIndex; col++) {
          ctx.drawImage(
            tiles[row * tileCols + col], // which part
            col * tw - x - 1, // x position
            row * th - y - 1 // y position
          );
        }
      }
    }
  };
}

function init() {
  console.log('image loaded');

  var bg = splitBigImage(bigImg, 250); // image_source, maxSize
  var ctx = document.getElementById('canvas').getContext('2d');
  var dx = 1,
    dy = 1,
    x = 150,
    y = 150;
  anim();
  setInterval(changeDirection, 2000);

  function anim() {
    // just to make the background position move...
    x += dx;
    y += dy;
    if (x < 0) {
      dx *= -1;
      x = 1;
    }
    if (x > bg.width - ctx.canvas.width) {
      dx *= -1;
      x = bg.width - ctx.canvas.width - 1;
    }
    if (y < 0) {
      dy *= -1;
      y = 1;
    }
    if (y > bg.height - ctx.canvas.height) {
      dy *= -1;
      y = bg.height - ctx.canvas.height - 1;
    }
    ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
    if(chck.checked) {
      // that's how you call it
      bg.draw(ctx, x, y);
    }
    else {
      ctx.drawImage(bigImg, -x, -y);
    }
    requestAnimationFrame(anim);
  }

  function changeDirection() {
    dx = (Math.random()) * 5 * Math.sign(dx);
    dy = (Math.random()) * 5 * Math.sign(dy);
  }

  setTimeout(function() { console.clear(); }, 1000);
}
// produces a width * height pseudo-random svg image
function generateBigImage(width, height) {
  var str = '<svg width="' + width + '" height="' + height + '" xmlns="http://www.w3.org/2000/svg">',
    x, y;
  for (y = 0; y < height / 20; y++)
    for (x = 0; x < width / 20; x++)
      str += '<circle ' +
      'cx="' + ((x * 20) + 10) + '" ' +
      'cy="' + ((y * 20) + 10) + '" ' +
      'r="15" ' +
      'fill="hsl(' + (width * height / ((y * x) + width)) + 'deg, ' + (((width + height) / (x + y)) + 35) + '%, 50%)" ' +
      '/>';
  str += '</svg>';
  return new Blob([str], {
    type: 'image/svg+xml'
  });
}
<label>draw split <input type="checkbox" id="chck" checked></label>
<canvas id="canvas" width="800" height="800"></canvas>

และที่จริงแล้ว ในกรณีเฉพาะของคุณ ฉันจะจัดเก็บภาพที่แยกไว้บนเซิร์ฟเวอร์เป็นการส่วนตัว (ไม่ว่าจะเป็นในรูปแบบ svg เนื่องจากควรใช้แบนด์วิธน้อยกว่า) และสร้างไทล์จากแหล่งต่างๆ แต่ผมจะปล่อยให้ผู้อ่านเป็นแบบฝึกหัด

person Kaiido    schedule 22.05.2018
comment
จริงๆ แล้วฉันลงเอยด้วยการใช้วิธีการที่ค่อนข้างคล้ายกับวิธีนี้... เนื่องจากพื้นหลังของฉันเป็นรูปแบบที่ซ้ำกัน ฉันจึงหยิบเซลล์รูปแบบหนึ่งแล้ววาดมันหลายครั้ง ขอบคุณสำหรับการแก้ปัญหาของคุณ! - person jolaleye; 25.05.2018

ฉันขอแนะนำอย่างยิ่งให้ใช้ EaselJS เพื่อแสดงผลออบเจ็กต์หลายรายการ/ขนาดใหญ่เป็นแคนวาส HTML ไลบรารีนี้รองรับการแคชเวกเตอร์, webgl และอื่นๆ อีกมากมาย

นี่คือตัวอย่างของการแคชวัตถุเวกเตอร์หลายรายการลงในบิตแมป (คลิกช่องทำเครื่องหมายเพื่อเปรียบเทียบ): https://www.createjs.com/demos/easeljs/cache

คุณควรจะปฏิบัติต่อเวกเตอร์พื้นหลังของคุณได้ในลักษณะเดียวกัน เพียงแคชวัตถุแล้วปล่อยให้ซอฟต์แวร์จัดการงานหนักทั้งหมด

person doppler    schedule 18.05.2018
comment
การสาธิตครั้งนั้นน่าประทับใจมาก ฉันจะตรวจสอบเรื่องนี้ ขอบคุณ! - person jolaleye; 19.05.2018
comment
ไม่มีการสาธิตใดที่แสดงให้เห็นว่าจะปรับปรุงการวาดภาพขนาดใหญ่ได้อย่างไร กรณีของ OP เป็นภาพที่แรสเตอร์แล้วขนาด 15,000 * 15,000 พิกเซล กลยุทธ์การแคช EaselJS คือการหลีกเลี่ยงการเรียกเมธอด Path เดียวกันหลายครั้ง โดยการสร้างเวอร์ชันแรสเตอร์ นั่นจะไม่ช่วย OP - person Kaiido; 22.05.2018
comment
@kaiido นี่เป็นตัวอย่างที่ดีของการใช้ 'cacheCanvas' หลังจากที่เวกเตอร์กลายเป็นบิตแมป: stackoverflow.com/a/35991855 /2510368. ตามทฤษฎี คุณสามารถแคชเวกเตอร์ขนาดใหญ่ลงในบิตแมปขนาดใหญ่ จากนั้นแคชบิตแมปขนาดเล็กจากวัตถุแคนวาสนั้น - person doppler; 22.05.2018
comment
@Kaiido ฉันเข้าใจแล้ว สมมติว่ารูปภาพขนาด 15,000x15000 (เวกเตอร์หรือบิตแมป) เป็นแบบคงที่ คุณสามารถ 'cacheCanvas' รูปภาพเป็นขนาดประมาณ 800x600 แล้ววาดใหม่ในขนาดที่เล็กกว่ามาก - person doppler; 23.05.2018
comment
@doppler แต่รูปภาพจะต้องแสดงตามขนาดจริง อย่างไรก็ตาม ฉันจะหยุดโต้เถียงและยึดติดกับความคิดเห็นแรกของฉันซึ่งระบุว่าคำตอบนี้ไม่ได้กำหนดเป้าหมายไปยังคำถามที่ถูกถาม (การจัดการกับรูปภาพขนาดใหญ่ในผ้าใบ HTML5) - person Kaiido; 23.05.2018
comment
EaselJS ไม่สามารถแก้ปัญหาภาพต้นฉบับได้ แต่สุดท้ายฉันก็ใช้มันเพื่อประโยชน์อื่นๆ ดังนั้นขอขอบคุณสำหรับคำแนะนำ - person jolaleye; 25.05.2018