OpenCV.js: Компьютерное зрение в JavaScript для веб-разработчиков

OpenCV.js Компьютерное зрение в JavaScript для веб-разработчиков
Полное руководство по использованию OpenCV.js для создания приложений с компьютерным зрением в браузере. От основ до продвинутых техник распознавания образов.

Введение в OpenCV.js

Компьютерное зрение стало неотъемлемой частью современных веб-приложений, открывая новые горизонты для создания интерактивных пользовательских интерфейсов. OpenCV.js делает эти технологии доступными прямо в браузере, позволяя разработчикам создавать инновационные решения без необходимости серверной обработки. Давайте погрузимся в мир OpenCV.js и узнаем, как эта мощная библиотека может трансформировать ваши веб-проекты.

OpenCV.js представляет собой JavaScript-версию популярной библиотеки компьютерного зрения OpenCV, специально оптимизированную для работы в веб-браузерах. Эта технология позволяет обрабатывать изображения и видео прямо на стороне клиента, что открывает множество возможностей для создания интерактивных веб-приложений.

История и эволюция

История OpenCV.js началась с растущей потребности в обработке визуальных данных в веб-приложениях. Разработчики OpenCV, осознавая важность веб-платформы, решили перенести мощь компьютерного зрения в браузеры. Используя технологию WebAssembly, они создали производительное решение, способное работать практически на всех современных браузерах.

Ключевые преимущества

OpenCV.js обладает рядом уникальных преимуществ, которые делают его незаменимым инструментом для веб-разработчиков:

  1. Высокая производительность: Благодаря использованию WebAssembly, OpenCV.js обеспечивает скорость работы, близкую к нативным приложениям.
  2. Клиентская обработка: Вся обработка происходит на стороне клиента, что снижает нагрузку на сервер и улучшает приватность пользовательских данных.
  3. Совместимость с API: OpenCV.js сохраняет совместимость с основным API OpenCV, что облегчает переход для разработчиков, знакомых с библиотекой.
  4. Простая интеграция: Библиотеку легко интегрировать в существующие веб-проекты, что делает ее идеальным выбором для быстрого прототипирования и разработки.

Начало работы с OpenCV.js

Теперь, когда мы понимаем важность и преимущества OpenCV.js, давайте сделаем первые шаги в использовании этой мощной библиотеки.

Подключение библиотеки

Для начала работы с OpenCV.js необходимо добавить библиотеку в ваш проект. Самый простой способ — это использование CDN:


<!-- Подключение через CDN -->
<script async src="https://docs.opencv.org/master/opencv.js"></script>

Базовая структура HTML

Создадим простую HTML-структуру для нашего первого проекта:


<!DOCTYPE html>
<html>
<head>
    <title>OpenCV.js Example</title>
</head>
<body>
    <canvas id="canvasInput"></canvas>
    <canvas id="canvasOutput"></canvas>
</body>
</html>

Инициализация OpenCV

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


// Ждем загрузки OpenCV
cv['onRuntimeInitialized'] = () => {
    console.log('OpenCV.js загружен');
    // Здесь начинаем работу с OpenCV.js
};

Первые шаги: загрузка и обработка изображения

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


function processImage(imageSource) {
    let src = cv.imread(imageSource);
    let dst = new cv.Mat();

    // Простое преобразование в оттенки серого
    cv.cvtColor(src, dst, cv.COLOR_RGBA2GRAY);

    // Отображаем результат
    cv.imshow('canvasOutput', dst);

    // Освобождаем память
    src.delete();
    dst.delete();
}

Этот простой пример демонстрирует базовый рабочий процесс с OpenCV.js: загрузка изображения, его обработка и отображение результата. Обратите внимание на важность освобождения памяти после использования объектов Mat — это ключевая концепция при работе с OpenCV.js.

Важные концепции для начинающих

При работе с OpenCV.js следует помнить о нескольких ключевых моментах:

  1. Управление памятью: В отличие от обычного JavaScript, в OpenCV.js необходимо явно освобождать память, используя метод delete() для объектов Mat.
  2. Асинхронная загрузка: Библиотека загружается асинхронно, поэтому важно дождаться события onRuntimeInitialized перед началом работы.
  3. Интеграция с Canvas: OpenCV.js тесно интегрируется с HTML5 Canvas API, что позволяет эффективно обрабатывать и отображать изображения.

Понимание этих концепций заложит прочный фундамент для дальнейшего изучения более сложных техник обработки изображений с помощью OpenCV.js.

По мере углубления в мир компьютерного зрения с OpenCV.js, мы будем исследовать более сложные операции и алгоритмы. В следующем разделе мы рассмотрим основные концепции работы с изображениями и научимся применять базовые фильтры и преобразования. Готовы ли вы погрузиться глубже в захватывающий мир обработки изображений прямо в вашем браузере?

Основные концепции работы с OpenCV.js

Работа с матрицами изображений

В основе работы с OpenCV.js лежит концепция матриц (Mat объектов), которые представляют собой основной формат хранения и обработки изображений. Каждое изображение представляется в виде многомерного массива, где каждый пиксель содержит информацию о цвете или интенсивности.


// Создание новой матрицы
let mat = new cv.Mat();

// Создание матрицы определенного размера
let mat = new cv.Mat(height, width, cv.CV_8UC4);

Цветовые пространства

OpenCV.js поддерживает работу с различными цветовыми пространствами, что критически важно для различных задач обработки изображений:


function convertColor(src) {
    let dst = new cv.Mat();

    // Конвертация в оттенки серого
    cv.cvtColor(src, dst, cv.COLOR_RGBA2GRAY);

    // Конвертация в HSV
    // cv.cvtColor(src, dst, cv.COLOR_RGB2HSV);

    return dst;
}

Практические примеры обработки изображений

Детектирование границ

Одна из базовых операций в компьютерном зрении — это обнаружение границ на изображении. Вот пример использования детектора границ Canny:


function detectEdges(imageData) {
    let src = cv.imread(imageData);
    let dst = new cv.Mat();
    let edges = new cv.Mat();

    // Преобразование в оттенки серого
    cv.cvtColor(src, dst, cv.COLOR_RGBA2GRAY);

    // Применение размытия по Гауссу
    cv.GaussianBlur(dst, dst, new cv.Size(5, 5), 0, 0, cv.BORDER_DEFAULT);

    // Детектирование границ
    cv.Canny(dst, edges, 50, 150, 3, false);

    return edges;
}

Распознавание лиц

OpenCV.js включает в себя мощные инструменты для распознавания лиц. Вот пример базового распознавания:


async function detectFaces(imageElement) {
    let src = cv.imread(imageElement);
    let gray = new cv.Mat();
    cv.cvtColor(src, gray, cv.COLOR_RGBA2GRAY);

    let faces = new cv.RectVector();
    let classifier = new cv.CascadeClassifier();

    // Загрузка классификатора
    await classifier.load('haarcascade_frontalface_default.xml');

    // Поиск лиц
    classifier.detectMultiScale(gray, faces);

    // Отрисовка прямоугольников вокруг лиц
    for (let i = 0; i < faces.size(); ++i) {
        let face = faces.get(i);
        let point1 = new cv.Point(face.x, face.y);
        let point2 = new cv.Point(face.x + face.width, face.y + face.height);
        cv.rectangle(src, point1, point2, [255, 0, 0, 255], 2);
    }

    return src;
}

Обработка видео в реальном времени

Одно из главных преимуществ OpenCV.js - возможность обработки видео в реальном времени прямо в браузере:


function processVideo(videoElement, canvasElement) {
    let src = new cv.Mat(videoElement.height, videoElement.width, cv.CV_8UC4);
    let dst = new cv.Mat(videoElement.height, videoElement.width, cv.CV_8UC1);
    let cap = new cv.VideoCapture(videoElement);

    const FPS = 30;
    function processFrame() {
        try {
            cap.read(src);
            cv.cvtColor(src, dst, cv.COLOR_RGBA2GRAY);
            cv.imshow(canvasElement, dst);
            setTimeout(processFrame, 1000/FPS);
        } catch (err) {
            console.error(err);
        }
    }

    processFrame();
}

Компьютерное зрение открывает огромные возможности для веб-разработки, и OpenCV.js делает эти технологии доступными прямо в браузере. В следующих разделах мы рассмотрим более сложные алгоритмы обработки изображений и их практическое применение в реальных проектах. Готовы ли вы создать свое первое приложение с использованием компьютерного зрения?

Продвинутые техники работы с OpenCV.js

Оптимизация производительности

При работе с OpenCV.js важно учитывать производительность, особенно при обработке изображений в реальном времени. Вот несколько ключевых приемов оптимизации:


// Переиспользование Mat объектов
let processingMat = new cv.Mat();
let grayMat = new cv.Mat();

function processFrame() {
    // Используем существующие Mat объекты вместо создания новых
    cv.cvtColor(processingMat, grayMat, cv.COLOR_RGBA2GRAY);
    // Обработка...
}

Интеграция с WebAssembly

OpenCV.js использует WebAssembly для достижения высокой производительности. Вот пример оптимизированной обработки изображения:


async function optimizedProcessing(imageElement) {
    // Ждем полной загрузки WebAssembly модуля
    await cv.ready;

    let src = cv.imread(imageElement);
    let dst = new cv.Mat();

    // Используем SIMD-оптимизированные функции
    cv.cvtColor(src, dst, cv.COLOR_RGBA2GRAY, 0);

    return dst;
}

Практические примеры продвинутых техник

Сегментация изображений

Сегментация - это мощный инструмент для выделения объектов на изображении:


function segmentImage(src) {
    let markers = new cv.Mat();
    let dst = new cv.Mat();

    // Подготовка изображения
    cv.pyrMeanShiftFiltering(src, dst, 21, 51);

    // Создание маркеров
    cv.cvtColor(dst, markers, cv.COLOR_RGBA2GRAY);
    cv.threshold(markers, markers, 0, 255, cv.THRESH_BINARY + cv.THRESH_OTSU);

    // Применение watershed алгоритма
    cv.watershed(dst, markers);

    return markers;
}

Морфологические операции

Морфологические операции помогают улучшить качество распознавания объектов:


function morphologicalOperations(src) {
    let dst = new cv.Mat();
    let kernel = cv.getStructuringElement(cv.MORPH_RECT, new cv.Size(5, 5));

    // Применение операций
    cv.morphologyEx(src, dst, cv.MORPH_OPEN, kernel);
    cv.morphologyEx(dst, dst, cv.MORPH_CLOSE, kernel);

    kernel.delete();
    return dst;
}

Адаптивная бинаризация

Этот метод особенно полезен при работе с изображениями с неравномерным освещением:


function adaptiveThreshold(src) {
    let dst = new cv.Mat();
    let gray = new cv.Mat();

    // Преобразование в оттенки серого
    cv.cvtColor(src, gray, cv.COLOR_RGBA2GRAY);

    // Применение адаптивного порога
    cv.adaptiveThreshold(gray, dst, 255,
        cv.ADAPTIVE_THRESH_GAUSSIAN_C,
        cv.THRESH_BINARY,
        11, 2);

    gray.delete();
    return dst;
}

Советы и лучшие практики

Управление памятью

При работе с OpenCV.js критически важно правильно управлять памятью:


function properMemoryManagement() {
    let mats = [];

    try {
        // Создаем необходимые матрицы
        mats.push(new cv.Mat());
        mats.push(new cv.Mat());

        // Обработка...

    } finally {
        // Освобождаем память
        mats.forEach(mat => mat.delete());
    }
}

Обработка ошибок

Правильная обработка ошибок поможет создать надежные приложения:


function robustImageProcessing(src) {
    let result = null;
    let tempMats = [];

    try {
        let gray = new cv.Mat();
        tempMats.push(gray);

        cv.cvtColor(src, gray, cv.COLOR_RGBA2GRAY);
        // Дополнительная обработка...

        result = gray.clone();
    } catch (error) {
        console.error('Ошибка обработки изображения:', error);
    } finally {
        tempMats.forEach(mat => mat.delete());
    }

    return result;
}

Компьютерное зрение с OpenCV.js открывает широкие возможности для создания современных веб-приложений. Используя описанные техники и следуя лучшим практикам, вы сможете создавать эффективные и надежные решения для обработки изображений прямо в браузере. В следующих материалах мы рассмотрим более сложные алгоритмы и их практическое применение в реальных проектах.

Интеграция с современными веб-технологиями

Взаимодействие с React

Использование OpenCV.js в React-приложениях требует особого подхода к управлению жизненным циклом компонентов:


function OpenCVComponent() {
    const canvasRef = useRef(null);

    useEffect(() => {
        // Инициализация OpenCV
        const initOpenCV = async () => {
            await cv.ready;

            // Начало обработки
            const canvas = canvasRef.current;
            const ctx = canvas.getContext('2d');

            // Код обработки...
        };

        initOpenCV();

        // Очистка при размонтировании
        return () => {
            // Освобождение ресурсов
        };
    }, []);

    return (
        <canvas ref={canvasRef}></canvas>
    );
}

Работа с WebRTC

Интеграция OpenCV.js с WebRTC позволяет создавать приложения для обработки видео в реальном времени:


async function setupVideoProcessing() {
    try {
        const stream = await navigator.mediaDevices.getUserMedia({
            video: true
        });

        const videoElement = document.createElement('video');
        videoElement.srcObject = stream;
        videoElement.play();

        videoElement.onloadedmetadata = () => {
            processVideoStream(videoElement);
        };
    } catch (error) {
        console.error('Ошибка доступа к камере:', error);
    }
}

function processVideoStream(video) {
    const src = new cv.Mat(video.height, video.width, cv.CV_8UC4);
    const dst = new cv.Mat(video.height, video.width, cv.CV_8UC4);

    function processFrame() {
        try {
            const begin = Date.now();

            // Захват кадра
            const context = canvas.getContext('2d');
            context.drawImage(video, 0, 0, video.width, video.height);
            src.data.set(context.getImageData(0, 0, video.width, video.height).data);

            // Обработка кадра
            cv.cvtColor(src, dst, cv.COLOR_RGBA2GRAY);
            cv.imshow('outputCanvas', dst);

            // Расчет FPS
            const delay = 1000/30 - (Date.now() - begin);
            setTimeout(processFrame, delay);
        } catch (err) {
            console.error('Ошибка обработки кадра:', err);
        }
    }

    processFrame();
}

Оптимизация для мобильных устройств

При разработке для мобильных платформ важно учитывать ограничения ресурсов:


function mobileOptimizedProcessing(src) {
    // Уменьшаем размер изображения для обработки
    let resized = new cv.Mat();
    let dsize = new cv.Size(src.cols / 2, src.rows / 2);
    cv.resize(src, resized, dsize, 0, 0, cv.INTER_AREA);

    // Обработка уменьшенного изображения
    let processed = new cv.Mat();
    cv.cvtColor(resized, processed, cv.COLOR_RGBA2GRAY);

    // Возвращаем к исходному размеру
    let result = new cv.Mat();
    dsize = new cv.Size(src.cols, src.rows);
    cv.resize(processed, result, dsize, 0, 0, cv.INTER_LINEAR);

    // Очистка
    resized.delete();
    processed.delete();

    return result;
}

Практические кейсы применения

Распознавание текста на изображениях


async function detectText(imageElement) {
    let src = cv.imread(imageElement);
    let gray = new cv.Mat();
    let binary = new cv.Mat();

    // Предобработка изображения
    cv.cvtColor(src, gray, cv.COLOR_RGBA2GRAY);
    cv.threshold(gray, binary, 0, 255, cv.THRESH_BINARY + cv.THRESH_OTSU);

    // Поиск контуров текста
    let contours = new cv.MatVector();
    let hierarchy = new cv.Mat();
    cv.findContours(binary, contours, hierarchy, 
        cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE);

    // Анализ найденных областей
    for (let i = 0; i < contours.size(); ++i) {
        let rect = cv.boundingRect(contours.get(i));
        cv.rectangle(src, 
            new cv.Point(rect.x, rect.y),
            new cv.Point(rect.x + rect.width, rect.y + rect.height),
            [0, 255, 0, 255],
            2);
    }

    // Очистка
    gray.delete();
    binary.delete();
    contours.delete();
    hierarchy.delete();

    return src;
}

Отслеживание движения


function motionDetection(prevFrame, currentFrame) {
    let diff = new cv.Mat();
    let thresh = new cv.Mat();

    // Вычисление разницы между кадрами
    cv.absdiff(prevFrame, currentFrame, diff);
    cv.threshold(diff, thresh, 25, 255, cv.THRESH_BINARY);

    // Удаление шума
    let kernel = cv.getStructuringElement(cv.MORPH_RECT, new cv.Size(3, 3));
    cv.erode(thresh, thresh, kernel, new cv.Point(-1, -1), 2);
    cv.dilate(thresh, thresh, kernel, new cv.Point(-1, -1), 2);

    // Поиск контуров движения
    let contours = new cv.MatVector();
    let hierarchy = new cv.Mat();
    cv.findContours(thresh, contours, hierarchy,
        cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE);

    // Очистка
    diff.delete();
    kernel.delete();
    hierarchy.delete();

    return { thresh, contours };
}

Компьютерное зрение становится все более важным инструментом в современной веб-разработке. OpenCV.js предоставляет мощные возможности для создания интерактивных приложений с обработкой изображений и видео прямо в браузере. Применяя описанные техники и следуя лучшим практикам, вы сможете создавать эффективные решения для широкого спектра задач - от простой обработки изображений до сложных систем компьютерного зрения.

Расширенные алгоритмы и их применение

Детектирование объектов с помощью HOG

Гистограмма направленных градиентов (HOG) - это эффективный метод для обнаружения объектов:


function detectObjectsHOG(src) {
    let gray = new cv.Mat();
    let gradient_x = new cv.Mat();
    let gradient_y = new cv.Mat();

    // Преобразование в оттенки серого
    cv.cvtColor(src, gray, cv.COLOR_RGBA2GRAY);

    // Вычисление градиентов
    cv.Sobel(gray, gradient_x, cv.CV_64F, 1, 0, 3);
    cv.Sobel(gray, gradient_y, cv.CV_64F, 0, 1, 3);

    // Вычисление магнитуды и направления
    let magnitude = new cv.Mat();
    let angle = new cv.Mat();
    cv.cartToPolar(gradient_x, gradient_y, magnitude, angle);

    // Очистка ресурсов
    gray.delete();
    gradient_x.delete();
    gradient_y.delete();

    return { magnitude, angle };
}

Машинное обучение с OpenCV.js


async function trainClassifier(trainingData) {
    let svm = new cv.SVM();

    // Подготовка данных
    let trainFeatures = new cv.Mat(trainingData.length, 2, cv.CV_32F);
    let trainLabels = new cv.Mat(trainingData.length, 1, cv.CV_32S);

    // Заполнение данных
    for (let i = 0; i < trainingData.length; i++) {
        trainFeatures.data32F[i * 2] = trainingData[i].x;
        trainFeatures.data32F[i * 2 + 1] = trainingData[i].y;
        trainLabels.data32S[i] = trainingData[i].label;
    }

    // Настройка параметров SVM
    let svmParams = new cv.SVMParams();
    svmParams.kernelType = cv.SVM_LINEAR;
    svmParams.svmType = cv.SVM_C_SVC;

    // Обучение модели
    svm.train(trainFeatures, trainLabels, new cv.Mat(), new cv.Mat(), svmParams);

    return svm;
}

Оптимизация производительности и отладка

Профилирование производительности


class PerformanceMonitor {
    constructor() {
        this.frameCount = 0;
        this.fps = 0;
        this.lastTime = performance.now();
    }

    update() {
        const now = performance.now();
        this.frameCount++;

        if (now - this.lastTime >= 1000) {
            this.fps = this.frameCount;
            this.frameCount = 0;
            this.lastTime = now;
        }

        return this.fps;
    }
}

function monitorProcessing(processFunction) {
    const monitor = new PerformanceMonitor();

    return function(...args) {
        const startTime = performance.now();
        const result = processFunction(...args);
        const endTime = performance.now();

        const fps = monitor.update();
        console.log(`Время обработки: ${endTime - startTime}ms, FPS: ${fps}`);

        return result;
    };
}

Отладка визуальных алгоритмов


function debugVisualization(src, steps = []) {
    let results = [];
    let current = src.clone();

    try {
        for (let step of steps) {
            let result = step.process(current);
            results.push({
                name: step.name,
                image: result.clone()
            });
            current = result;
        }

        // Создание визуализации
        let debug = new cv.Mat();
        cv.hconcat(results.map(r => r.image), debug);

        return {
            finalResult: current,
            debugView: debug,
            steps: results
        };
    } finally {
        // Очистка промежуточных результатов
        results.forEach(r => r.image.delete());
    }
}

Практические рекомендации по развертыванию

Оптимизация загрузки библиотеки


function loadOpenCV() {
    return new Promise((resolve, reject) => {
        const script = document.createElement('script');
        script.src = 'https://docs.opencv.org/master/opencv.js';
        script.async = true;
        script.onload = () => {
            cv['onRuntimeInitialized'] = () => {
                resolve(cv);
            };
        };
        script.onerror = () => {
            reject(new Error('Ошибка загрузки OpenCV.js'));
        };
        document.body.appendChild(script);
    });
}

// Использование с прогрессом загрузки
async function initializeOpenCV(progressCallback) {
    try {
        const opencv = await loadOpenCV();
        progressCallback(100);
        return opencv;
    } catch (error) {
        console.error('Ошибка инициализации OpenCV:', error);
        progressCallback(0);
        throw error;
    }
}

Обработка ошибок и восстановление


class OpenCVProcessor {
    constructor() {
        this.errorCount = 0;
        this.maxErrors = 3;
    }

    async processWithRecovery(src, processFunction) {
        try {
            if (this.errorCount >= this.maxErrors) {
                throw new Error('Превышено максимальное количество ошибок');
            }

            const result = await processFunction(src);
            this.errorCount = 0;
            return result;

        } catch (error) {
            this.errorCount++;
            console.error(`Ошибка обработки (попытка ${this.errorCount}):`, error);

            if (this.errorCount < this.maxErrors) {
                // Повторная попытка с задержкой
                await new Promise(resolve => setTimeout(resolve, 1000));
                return this.processWithRecovery(src, processFunction);
            }

            throw error;
        }
    }
}

Завершим наше погружение в мир OpenCV.js практическим советом: начните с простых задач, постепенно усложняя их по мере освоения библиотеки. Создайте свой первый проект по обработке изображений, затем перейдите к видео в реальном времени, и в конце концов вы сможете реализовать сложные системы компьютерного зрения. Главное — не бояться экспериментировать и постоянно практиковаться в написании кода.

Путь к мастерству в OpenCV.js

Представьте, что вы только что получили мощный инструмент, способный превратить обычный браузер в настоящую лабораторию компьютерного зрения. OpenCV.js — это не просто библиотека, это ключ к созданию удивительных веб-приложений, которые еще недавно казались научной фантастикой.

С чего начать свой путь

Начните с простого проекта — например, создайте веб-приложение для обработки селфи с применением различных фильтров. Такой проект поможет освоить базовые концепции и принципы работы с изображениями:


function createPhotoFilter(imageElement) {
    let src = cv.imread(imageElement);
    let dst = new cv.Mat();

    // Создаем художественный эффект
    cv.stylization(src, dst, 60, 0.07);

    // Добавляем теплый оттенок
    let channels = new cv.MatVector();
    cv.split(dst, channels);
    channels.get(2).convertTo(channels.get(2), -1, 1.2, 0);
    cv.merge(channels, dst);

    return dst;
}

Следующий уровень

Когда базовые операции станут привычными, переходите к более сложным проектам. Например, создайте систему распознавания жестов для управления веб-интерфейсом или разработайте умный фоторедактор с автоматическим улучшением качества фотографий.

Ваш личный путь развития

Вот три ключевых направления для совершенствования навыков:

  1. Алгоритмическое мышление: Изучайте не только как использовать готовые функции OpenCV.js, но и понимайте принципы работы алгоритмов компьютерного зрения.
  2. Оптимизация: Учитесь писать эффективный код, который работает быстро даже на мобильных устройствах.
  3. Творческий подход: Экспериментируйте с различными комбинациями алгоритмов для создания уникальных эффектов и решений.

Последний совет

Помните, что каждый великий проект начинается с простой строчки кода. Не бойтесь ошибок — они ваши лучшие учителя. Создавайте, экспериментируйте и делитесь своими достижениями с сообществом разработчиков. Ведь именно из таких экспериментов рождаются инновации, которые меняют мир веб-разработки.

Теперь, вооружившись знаниями из этого руководства, вы готовы начать свое путешествие в мир компьютерного зрения. Пришло время открыть редактор кода и написать свои первые строки с OpenCV.js. Удачи в ваших проектах!

Понравилась запись? Оцените!
Оценок: 1 Среднее: 5
Telegram
WhatsApp
VK
Facebook
Email

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Рекомендуем