Как получить положение прямоугольника на холсте после преобразования холста

#javascript #react-native #tensorflow #canvas

#javascript #react-native #тензорный поток #холст

Вопрос:

Я пытаюсь нарисовать прямоугольник с надписью чуть выше него на холсте. X, y, ширина и высота были сгенерированы так, чтобы быть вокруг объекта, который был обнаружен с помощью модели coco-ssd в tensorflow. Проблема в том, что координаты, сгенерированные моделью coco-ssd в tensorflow, относятся к источнику, отличному от самого холста. Более конкретно, начало координат для модели coco-ssd находится в верхнем правом углу, а начало координат для холста — в верхнем левом углу.

Я могу перемещать начало координат холста, но не происхождение модели (о котором я знаю). Чтобы переместить начало координат холста, я перевел холст вправо на 410 пикселей, на 4 пикселя меньше ширины холста, а затем отразил его горизонтально. Это рисует прямоугольник в правильном положении. Если бы я также должен был создать текст на этом этапе, он был бы перевернут и нечитаем (но в правильном положении). Если бы можно было получить положение прямоугольника по x и y после перевода холста обратно влево на 410 пикселей и еще раз отразить его по горизонтали, я мог бы легко использовать эти координаты для заполнения текста в нужном положении. Из того, что я узнал о холсте, это невозможно. (Пожалуйста, поправьте меня, если я ошибаюсь)

Другим решением, которое я рассмотрел, было бы использовать сгенерированную позицию x и применить эту формулу, -x xLim, где xLim — максимально возможное значение x. Проблема здесь в том, что получение xLim также невозможно, оно не является статичным, и оно будет меняться в зависимости от расстояния до обнаруженного объекта. Я знаю это, пытаясь получить, каким может быть xLim, просто расположив объект в крайнем левом углу экрана. (Наибольшее значение x, которое в настоящее время доступно для просмотра относительно происхождения модели coco-ssd) Имейте в виду, что если я создам расстояние от объекта, значение x в крайнем левом углу экрана увеличится. Если бы я смог каким-то образом получить наибольшее значение x, которое активно просматривается на холсте, тогда это было бы еще одним жизнеспособным решением.

Вот функция, отвечающая за рисование на холсте.

 export const drawRect = (x, y, width, height, text, ctx, canvas) => {
    ctx.clearRect(0, 0, canvas.current.width, canvas.current.height);
    ctx.transform(-1, 0, 0, 1, 410, 0);

        //draw rectangle
        ctx.beginPath();        
        const r = ctx.rect(x,y,width,height)
        ctx.stroke()

        //draw text
        ctx.save();
        ctx.scale(-1,1);
        ctx.translate(-410, 0)
        //update x and y to point to where the rectangle is currently
        ctx.fillText(text,x,y-5)
        ctx.stroke()
        ctx.restore()
    })
 

Я чувствую себя сильно ограниченным API, доступным для react native, и я надеюсь, что есть что-то, что я просто упустил из виду. Я потратил много времени, пытаясь решить эту проблему, и нашел много связанных вопросов о переполнении стека, но ни один из них не дал представления о том, как решить эту проблему с таким количеством неизвестных переменных.

Вот несколько изображений, чтобы наглядно представить проблему.

Крайний левый

Самый правый

Без восстановления исходного состояния холста

Краткие сведения:

Начало координат для модели coco-ssd находится в верхнем правом углу, а начало координат для холста — в верхнем левом углу.

Мне нужно

A.) Каким-то образом захватить наибольшее значение x, которое активно просматривается на холсте

или

B.) получите положение прямоугольника по x и y после перевода холста обратно влево на 410 пикселей и еще раз отразите его по горизонтали

Это в среде react native expo

Публичное репозиторий: https://github.com/dpaceoffice/MobileAppTeamProject

Ответ №1:

Используемые вами преобразования кажутся ненужными. Вот простое доказательство концепции с использованием кода встраивания для coco-ssd и примера по работе с видео:

Следующий код не использует никаких преобразований, и все работает так, как ожидалось. Видео, холст и модель используют одну и ту же систему координат:

  • (0, 0) — верхний левый угол рамки
  • (ширина, 0) находится вверху справа
  • (0, высота) находится внизу слева
  • (ширина, высота) внизу справа

И прямоугольник (x, y, ширина, высота) также везде использует знакомые координаты:

  • (x, y) находится вверху слева
  • (x width, y) находится вверху справа
  • (x, y высота) находится внизу слева
  • (x width, y height) находится внизу справа

Единственное, что немного отличается, это метод context.fillText(text, x, y):

  • (x, y) — нижний левый угол текста. Тем не менее, это на самом деле приятно, потому что мы просто можем нарисовать прямоугольник и текст с одинаковыми координатами, и текст будет располагаться прямо над прямоугольником.
  • (x, y — размер шрифта) обычно находится в верхнем левом углу
  • (x, y FontSize) — это координата для следующей строки.

Если вы хотите поместить текст в другую позицию, context.measureText(текст) может представлять интерес. Он возвращает объект TextMetrics. Обычно наибольший интерес представляет свойство .width этого объекта:

  • (x context.measureText(text).width, y) — это нижний правый угол текста.
 const processor = {}

processor.doLoad = function (model)
{
    const video = document.getElementById("video")
    this.video = video

    this.canvas = document.getElementById("canvas")
    this.context = this.canvas.getContext("2d")

    this.model = model

    video.addEventListener("play", () => {
        this.canvas.width = video.videoWidth
        this.canvas.height = video.videoHeight
        this.timerCallback()
    }, false)
}

processor.timerCallback = async function ()
{
    if (this.video.paused || this.video.ended)
        return
    await this.computeFrame()
    setTimeout(() => this.timerCallback(), 0)
};

processor.computeFrame = async function ()
{
    // detect objects in the image.
    const predictions = await this.model.detect(this.video)

    const context = this.context
    // draws the frame from the video at position (0, 0)
    context.drawImage(this.video, 0, 0)

    context.strokeStyle = "red"
    context.fillStyle = "red"
    context.font = "16px sans-serif"
    for (const { bbox: [x, y, width, height], class: _class, score } of predictions)
    {
        // draws a rect with top-left corner of (x, y)
        context.strokeRect(x, y, width, height)
        // writes the class directly above (x, y), outside the rectangle
        context.fillText(_class, x, y)
        // writes the class directly below (x, y), inside the rectangle
        context.fillText(score.toFixed(2), x, y   16)
    }
}

// Load the model.
const model = cocoSsd.load()
model.then(model => processor.doLoad(model)) 
 <!-- Load TensorFlow.js. This is required to use coco-ssd model. -->
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs"> </script>
<!-- Load the coco-ssd model. -->
<script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/coco-ssd"> </script>

<div style="display: flex; flex-flow: column; gap: 1em; width: 200px;">
    <!-- Replace this with your image. Make sure CORS settings allow reading the image! -->
    <video id="video" src="https://mdn.github.io/dom-examples/canvas/chroma-keying/media/video.mp4" controls crossorigin="anonymous"></video>

    <canvas id="canvas" style="border: 1px solid black;"></canvas>
</div> 

Поскольку кажется, что вы хотите нарисовать изображение в какой-то части холста, вот некоторый код, который демонстрирует, как правильно использовать преобразования. Здесь есть два варианта:

  1. Используйте преобразования (1x translate 1x scale), чтобы вы могли использовать систему координат из изображения. Обратите внимание, что для этого не требуются отрицательные масштабы. Рисование обрабатывается браузером. Вы должны исправить шрифт и ширину строки для масштабирования.
  2. Также используйте преобразования (1x translate 1x scale). Затем восстановите холст и преобразуйте точки вручную. Это требует некоторой дополнительной математики для преобразования точек. Однако плюсом является то, что вам не нужно корректировать шрифт и ширину строк.
 const image = document.createElement("canvas")
image.width = 1200
image.height = 600

const image_context = image.getContext("2d")
image_context.fillStyle = "#4af"
image_context.fillRect(0, 0, 1200, 600)

const circle = (x, y, r) =>
{
    image_context.beginPath()
    image_context.arc(x, y, r, 0, Math.PI*2)
    image_context.fill()
}

image_context.fillStyle = "#800"
image_context.fillRect(500-40/2, 400, 40, 180)

image_context.fillStyle = "#080"
circle(500, 400, 100)
circle(500, 300, 70)
circle(500, 220, 50)

const prediction = { bbox: [500-100, 220-50, 100*2, (400 180)-(220-50)], class: "tree", score: 0.42 } // somehow get a prediction

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

// we want to draw the big image into a smaller area (x, y, width, height)
const x = 50
const y = 80
const width = 220
const height = width * (image.height / image.width)

// debug: mark the area that we want to draw the image into
{
    context.save() // save context, so that stroke properties can be restored
    context.lineWidth = 5
    context.strokeStyle = "red"
    context.setLineDash([10, 10])
    context.strokeRect(x, y, width, height)
    context.restore()
}

{
    // save context, before doing any transforms (even before setTransform)
    context.save()

    // Move top left corner to (x, y)
    context.translate(x, y)

    // This is the scale factor, it should be less than one, because the image is bigger that the target area. The idea is to increase the scale by the target area width, and then decrease the scale by the image width.
    const f = width / image.width
    context.scale(f, f)

    // Draws the image, note that the coordinates are just (0, 0) without scaling.
    context.drawImage(image, 0, 0)

    // option 1: draw the prediction using the native transforms
    if (true)
    {
        context.strokeStyle = "red"
        context.fillStyle = "red"
        context.lineWidth = 1 / f // linewidth and font-size has to be adjusted by scaling
        const fontSize = 16 / f
        context.font = fontSize.toFixed(0)   "px sans-serif"
        const [p_x, p_y, p_width, p_height] = prediction.bbox
        context.strokeRect(p_x, p_y, p_width, p_height) // draw the prediction
        context.fillText(prediction.class, p_x, p_y) // draw the text
        context.fillText(prediction.score.toFixed(2), p_x, p_y   fontSize) // draw the text
    }

    const matrix = context.getTransform() // save transform for option 2, needs to be done before restore()

    context.restore()

    // option 2: draw the prediction by manually transforming the corners
    if (false)
    {
        context.save()

        context.strokeStyle = "red"
        context.fillStyle = "red"
        context.lineWidth = 1
        const fontSize = 16
        context.font = fontSize   "px sans-serif"

        let [p_x, p_y, p_width, p_height] = prediction.bbox
        // manually transform corners
        const topleft = matrix.transformPoint(new DOMPoint(p_x, p_y))
        const bottomright = matrix.transformPoint(new DOMPoint(p_x   p_width, p_y   p_height))
        p_x = topleft.x
        p_y = topleft.y
        p_width = bottomright.x - topleft.x
        p_height = bottomright.y - topleft.y

        context.strokeRect(p_x, p_y, p_width, p_height) // draw the prediction
        context.fillText(prediction.class, p_x, p_y) // draw the text
        context.fillText(prediction.score.toFixed(2), p_x, p_y   fontSize) // draw the text

        context.restore()
    }
} 
 <canvas id="canvas" width=400 height=300 style="border: 1px solid black;"></canvas> 

Теперь, если вам нужно отразить изображение с помощью преобразований, вам просто нужно обязательно отразить его обратно перед написанием текста.

 const image = document.createElement("canvas")
image.width = 1200
image.height = 600

const image_context = image.getContext("2d")
image_context.fillStyle = "#4af"
image_context.fillRect(0, 0, 1200, 600)

const circle = (x, y, r) =>
{
    image_context.beginPath()
    image_context.arc(x, y, r, 0, Math.PI*2)
    image_context.fill()
}

image_context.fillStyle = "#800"
image_context.fillRect(500-40/2, 400, 40, 180)

image_context.fillStyle = "#080"
circle(500, 400, 100)
circle(500, 300, 70)
circle(500, 220, 50)

const prediction = { bbox: [500-100, 220-50, 100*2, (400 180)-(220-50)], class: "tree", score: 0.42 } // somehow get a prediction

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

// we want to draw the big image into a smaller area (x, y, width, height)
const x = 50
const y = 80
const width = 220
const height = width * (image.height / image.width)

// debug: mark the area that we want to draw the image into
{
    context.save() // save context, so that stroke properties can be restored
    context.lineWidth = 5
    context.strokeStyle = "red"
    context.setLineDash([10, 10])
    context.strokeRect(x, y, width, height)
    context.restore()
}

{
    // save context, before doing any transforms (even before setTransform)
    context.save()

    // Move top left corner to (x, y)
    context.translate(x, y)

    // This is the scale factor, it should be less than one, because the image is bigger that the target area. The idea is to increase the scale by the target area width, and then decrease the scale by the image width.
    const f = width / image.width
    context.scale(f, f)

    // mirror the image before drawing it
    context.scale(-1, 1)
    context.translate(-image.width, 0)

    // Draws the image, note that the coordinates are just (0, 0) without scaling.
    context.drawImage(image, 0, 0)

    // option 1: draw the prediction using the native transforms
    if (true)
    {
        const [p_x, p_y, p_width, p_height] = prediction.bbox
        // move to correct position and only then undo the mirroring
        context.save()
        context.translate(p_x   p_width, p_y) // move to top "right" (that is now actually at the left, due to mirroring)
        context.scale(-1, 1)

        context.strokeStyle = "red"
        context.fillStyle = "red"
        context.lineWidth = 1 / f // linewidth and font-size has to be adjusted by scaling
        const fontSize = 16 / f
        context.font = fontSize.toFixed(0)   "px sans-serif"
        context.strokeRect(0, 0, p_width, p_height) // draw the prediction
        context.fillText(prediction.class, 0, 0) // draw the text
        context.fillText(prediction.score.toFixed(2), 0, 0   fontSize) // draw the text
        context.restore()
    }

    const matrix = context.getTransform() // save transform for option 2, needs to be done before restore()

    context.restore()

    // option 2: draw the prediction by manually transforming the corners
    if (false)
    {
        context.save()

        context.strokeStyle = "red"
        context.fillStyle = "red"
        context.lineWidth = 1
        const fontSize = 16
        context.font = fontSize   "px sans-serif"

        let [p_x, p_y, p_width, p_height] = prediction.bbox
        // manually transform corners, note that compared to previous snippet, topleft now uses the top right corner of the rectangle
        const topleft = matrix.transformPoint(new DOMPoint(p_x   p_width, p_y))
        const bottomright = matrix.transformPoint(new DOMPoint(p_x, p_y   p_height))
        p_x = topleft.x
        p_y = topleft.y
        p_width = bottomright.x - topleft.x
        p_height = bottomright.y - topleft.y

        context.strokeRect(p_x, p_y, p_width, p_height) // draw the prediction
        context.fillText(prediction.class, p_x, p_y) // draw the text
        context.fillText(prediction.score.toFixed(2), p_x, p_y   fontSize) // draw the text

        context.restore()
    }
} 
 <canvas id="canvas" width=400 height=300 style="border: 1px solid black;"></canvas> 

Комментарии:

1. «Преобразования, которые вы используете, кажутся ненужными» Преобразования были моей попыткой переместить начало координат холста в модель coco-ssd. Который действительно поместил прямоугольник в правильное положение, без него и текст, и прямоугольник были переведены полностью по сравнению с тем, где они должны быть. В этом вопросе помечен React-native. Пожалуйста, просмотрите мои правки, чтобы было более четко известно, что это среда react native. Я прикрепил репозиторий проекта git, чтобы помочь воспроизвести проблему.