Подтвердить что ты не робот

Программно определить лучший цвет переднего плана, который будет помещен в изображение

Я работаю над модулем node, который вернет цвет, который будет выглядеть лучше всего на фоновом изображении, которое, конечно же, будет иметь несколько цветов.

Вот что я до сих пор:

'use strict';

var randomcolor = require('randomcolor');
var tinycolor = require('tinycolor2');

module.exports = function(colors, tries) {
  var topColor, data = {};

  if (typeof colors == 'string') { colors = [colors]; }
  if (!tries) { tries = 10000; }

  for (var t = 0; t < tries; t++) {
    var score = 0, color = randomcolor(); //tinycolor.random();

    for (var i = 0; i < colors.length; i++) {
      score += tinycolor.readability(colors[i], color);
    }

    data[color] = (score / colors.length);

    if (!topColor || data[color] > data[topColor]) {
      topColor = color;
    }
  }

  return tinycolor(topColor);
};

Итак, как это работает, я сначала предоставляю этот script 6 наиболее доминирующими цветами в изображении следующим образом:

[ { r: 44, g: 65, b: 54 },
  { r: 187, g: 196, b: 182 },
  { r: 68, g: 106, b: 124 },
  { r: 126, g: 145, b: 137 },
  { r: 147, g: 176, b: 169 },
  { r: 73, g: 138, b: 176 } ]

а затем он будет генерировать 10 000 различных случайных цветов, а затем выбрать тот, который имеет наилучший средний коэффициент контрастности с 6 заданными цветами.

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

С tinycolor2 я всегда получаю либо очень темный серый (почти черный), либо очень светло-серый (почти белый). И с randomcolor я либо закончил темно-синий, либо светлый персиковый цвет.

Мой script может не быть лучшим способом обойти это, но есть ли у кого-нибудь идеи?

Спасибо

4b9b3361

Ответ 1

Поиск доминирующего оттенка.

Представленный фрагмент показывает пример того, как найти доминирующий цвет. Он работает, разбивая изображение на его компоненты оттенка, насыщенности и яркости.

Уменьшение изображения

Чтобы ускорить процесс, изображение уменьшено до меньшего изображения (в данном случае 128 на 128 пикселей). Часть процесса восстановления также обрезает некоторые из внешних пикселей изображения.

const IMAGE_WORK_SIZE = 128;
const ICOUNT = IMAGE_WORK_SIZE * IMAGE_WORK_SIZE;
if(event.type === "load"){
    rImage = imageTools.createImage(IMAGE_WORK_SIZE, IMAGE_WORK_SIZE);  // reducing image
    c = rImage.ctx;
    // This is where you can crop the image. In this example I only look at the center of the image
    c.drawImage(this,-16,-16,IMAGE_WORK_SIZE + 32, IMAGE_WORK_SIZE + 32); // reduce image size

Найти среднюю яркость

После уменьшения я сканирую пиксели, преобразуя их в значения hsl и получая среднюю яркость.

Обратите внимание, что яркость является логарифмической шкалой, поэтому среднее значение является квадратным корнем из суммы квадратов, деленных на счет.

pixels = imageTools.getImageData(rImage).data;
l = 0;
for(i = 0; i < pixels.length; i += 4){ 
    hsl = imageTools.rgb2hsl(pixels[i],pixels[i + 1],pixels[i + 2]);
    l += hsl.l * hsl.l;
}
l = Math.sqrt(l/ICOUNT);

Гистограммы оттенков для диапазонов яркости и насыщенности.

Код может найти доминирующий цвет в диапазоне насыщенности и яркости. В этом примере я использую только одну степень, но вы можете использовать столько, сколько пожелаете. Используются только пиксели, находящиеся внутри диапазонов lum (яркость) и sat (насыщенность). Я записываю гистограмму оттенка для проходящих пикселей.

Пример диапазонов оттенков (один из)

hues = [{  // lum and sat have extent 0-100. high test is no inclusive hence high = 101 if you want the full range
        lum : {
            low :20,    // low limit lum >= this.lum.low
            high : 60,  // high limit lum < this.lum.high
            tot : 0,    // sum of lum values 
        },
        sat : { // all saturations from 0 to 100
            low : 0,
            high : 101,
            tot : 0, // sum of sat
        },
        count : 0, // count of pixels that passed
        histo : new Uint16Array(360), // hue histogram
    }]

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

hues[0].lum.low = l - 30;
hues[0].lum.high = l + 30;

Как только диапазон установлен, я получаю гистограмму оттенка для каждого диапазона (в этом случае)

for(i = 0; i < pixels.length; i += 4){ 
    hsl = imageTools.rgb2hsl(pixels[i],pixels[i + 1],pixels[i + 2]);
    for(j = 0; j < hues.length; j ++){
        hr = hues[j]; // hue range
        if(hsl.l >= hr.lum.low && hsl.l < hr.lum.high){
            if(hsl.s >= hr.sat.low && hsl.s < hr.sat.high){
                hr.histo[hsl.h] += 1;
                hr.count += 1;
                hr.lum.tot += hsl.l * hsl.l;
                hr.sat.tot += hsl.s;
            }
        }
    }
}

Взвешенный средний оттенок из гистограммы оттенков.

Затем, используя гистограмму, я нахожу взвешенный средний оттенок для диапазона

// get weighted hue for image
// just to simplify code hue 0 and 1 (reds) can combine
for(j = 0; j < hues.length; j += 1){
    hr = hues[j];
    wHue = 0;
    hueCount = 0;
    hr.histo[1] += hr.histo[0];
    for(i = 1; i < 360; i ++){
        wHue += (i) * hr.histo[i];
        hueCount += hr.histo[i];
    }
    h = Math.floor(wHue / hueCount);
    s = Math.floor(hr.sat.tot / hr.count);
    l = Math.floor(Math.sqrt(hr.lum.tot / hr.count));
    hr.rgb = imageTools.hsl2rgb(h,s,l);
    hr.rgba = imageTools.hex2RGBA(imageTools.rgba2Hex4(hr.rgb));
}

И вот об этом. Остальное - просто дисплей и прочее. Вышеприведенный код требует интерфейса imageTools (предоставляется), который имеет инструменты для управления изображениями.

Уродливое дополнение

То, что вы делаете с найденным цветом, зависит от вас. Если вам нужен дополнительный цвет, просто преобразуйте rgb в hsl imageTools.rgb2hsl и поверните тон 180 град, а затем преобразуйте обратно в rgb.

var hsl = imageTools.rgb2hsl(rgb.r, rgb.g, rgb.b);
hsl.h += 180;
var complementRgb = imageTools.rgb2hsl(hsl.h, hsl.s, hsl.l);

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

Демо "Пограничные птицы"

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

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

var images = [
   // "https://upload.wikimedia.org/wikipedia/commons/f/fe/Goldcrest_1.jpg",
   "https://upload.wikimedia.org/wikipedia/commons/thumb/2/22/Cistothorus_palustris_CT.jpg/450px-Cistothorus_palustris_CT.jpg",
    "https://upload.wikimedia.org/wikipedia/commons/thumb/3/37/Black-necked_Stilt_%28Himantopus_mexicanus%29%2C_Corte_Madera.jpg/362px-Black-necked_Stilt_%28Himantopus_mexicanus%29%2C_Corte_Madera.jpg",     
    "https://upload.wikimedia.org/wikipedia/commons/thumb/c/cc/Daurian_redstart_at_Daisen_Park_in_Osaka%2C_January_2016.jpg/573px-Daurian_redstart_at_Daisen_Park_in_Osaka%2C_January_2016.jpg",
    "https://upload.wikimedia.org/wikipedia/commons/thumb/d/da/Myioborus_torquatus_Santa_Elena.JPG/675px-Myioborus_torquatus_Santa_Elena.JPG",
    "https://upload.wikimedia.org/wikipedia/commons/thumb/e/ef/Great_tit_side-on.jpg/645px-Great_tit_side-on.jpg",
    "https://upload.wikimedia.org/wikipedia/commons/thumb/5/55/Sarcoramphus_papa_%28K%C3%B6nigsgeier_-_King_Vulture%29_-_Weltvogelpark_Walsrode_2013-01.jpg/675px-Sarcoramphus_papa_%28K%C3%B6nigsgeier_-_King_Vulture%29_-_Weltvogelpark_Walsrode_2013-01.jpg",,
    
];

function loadImageAddBorder(){
    if(images.length === 0){
        return ; // all done   
    }
    var imageSrc = images.shift();
    imageTools.loadImage(
        imageSrc,true,
        function(event){
            var pixels, topRGB, c, rImage, wImage, botRGB, grad, i, hsl, h, s, l, hues, hslMap, wHue, hueCount, j, hr, gradCols, border;
            const IMAGE_WORK_SIZE = 128;
            const ICOUNT = IMAGE_WORK_SIZE * IMAGE_WORK_SIZE;
            if(event.type === "load"){
                rImage = imageTools.createImage(IMAGE_WORK_SIZE, IMAGE_WORK_SIZE);  // reducing image
                c = rImage.ctx;
                // This is where you can crop the image. In this example I only look at the center of the image
                c.drawImage(this,-16,-16,IMAGE_WORK_SIZE + 32, IMAGE_WORK_SIZE + 32); // reduce image size
    
                pixels = imageTools.getImageData(rImage).data;
                h = 0;
                s = 0;
                l = 0;
                // these are the colour ranges you wish to look at
                hues = [{
                        lum : {
                            low :20,
                            high : 60,
                            tot : 0,
                        },
                        sat : { // all saturations
                            low : 0,
                            high : 101,
                            tot : 0,
                        },
                        count : 0,
                        histo : new Uint16Array(360),
                    }]
                for(i = 0; i < pixels.length; i += 4){ 
                    hsl = imageTools.rgb2hsl(pixels[i],pixels[i + 1],pixels[i + 2]);
                    l += hsl.l * hsl.l;
                }
                l = Math.sqrt(l/ICOUNT);
                hues[0].lum.low = l - 30;
                hues[0].lum.high = l + 30;
                for(i = 0; i < pixels.length; i += 4){ 
                    hsl = imageTools.rgb2hsl(pixels[i], pixels[i + 1], pixels[i + 2]);
                    for(j = 0; j < hues.length; j ++){
                        hr = hues[j]; // hue range
                        if(hsl.l >= hr.lum.low && hsl.l < hr.lum.high){
                            if(hsl.s >= hr.sat.low && hsl.s < hr.sat.high){
                                hr.histo[hsl.h] += 1;
                                hr.count += 1;
                                hr.lum.tot += hsl.l * hsl.l;
                                hr.sat.tot += hsl.s;
                            }
                        }
                    }
                }
                // get weighted hue for image
                // just to simplify code hue 0 and 1 (reds) can combine
                for(j = 0; j < hues.length; j += 1){
                    hr = hues[j];
                    wHue = 0;
                    hueCount = 0;
                    hr.histo[1] += hr.histo[0];
                    for(i = 1; i < 360; i ++){
                        wHue += (i) * hr.histo[i];
                        hueCount += hr.histo[i];
                    }
                    h = Math.floor(wHue / hueCount);
                    s = Math.floor(hr.sat.tot / hr.count);
                    l = Math.floor(Math.sqrt(hr.lum.tot / hr.count));
                    hr.rgb = imageTools.hsl2rgb(h,s,l);
                    hr.rgba = imageTools.hex2RGBA(imageTools.rgba2Hex4(hr.rgb));
                }
                gradCols = hues.map(h=>h.rgba);
                if(gradCols.length === 1){
                    gradCols.push(gradCols[0]); // this is a quick fix if only one colour the gradient needs more than one
                }
                border = Math.floor(Math.min(this.width / 10,this.height / 10, 64));
    
                wImage = imageTools.padImage(this,border,border);
                wImage.ctx.fillStyle = imageTools.createGradient(
                    c, "linear", 0, 0, 0, wImage.height,gradCols
                );
                wImage.ctx.fillRect(0, 0, wImage.width, wImage.height);
                wImage.ctx.fillStyle = "black";
                wImage.ctx.fillRect(border - 2, border - 2, wImage.width - border * 2 + 4, wImage.height - border * 2 + 4);           
                wImage.ctx.drawImage(this,border,border);
                wImage.style.width = (innerWidth -64) + "px";
                document.body.appendChild(wImage);
                setTimeout(loadImageAddBorder,1000);
            }
        }
        
    )
}

setTimeout(loadImageAddBorder,0);



/** ImageTools.js begin **/
var imageTools = (function () {
    // This interface is as is. 
    // No warenties no garenties, and 
    /*****************************/
    /* NOT to be used comercialy */
    /*****************************/
    var workImg,workImg1,keep; // for internal use
    keep = false; 
    const toHex = v => (v < 0x10 ? "0" : "") + Math.floor(v).toString(16);
    var tools = {
        canvas(width, height) {  // create a blank image (canvas)
            var c = document.createElement("canvas");
            c.width = width;
            c.height = height;
            return c;
        },
        createImage (width, height) {
            var i = this.canvas(width, height);
            i.ctx = i.getContext("2d");
            return i;
        },
        loadImage (url, crossSite, cb) { // cb is calback. Check first argument for status
            var i = new Image();
            if(crossSite){
                i.setAttribute('crossOrigin', 'anonymous');
            }
            i.src = url;
            i.addEventListener('load', cb);
            i.addEventListener('error', cb);
            return i;
        },
        image2Canvas(img) {
            var i = this.canvas(img.width, img.height);
            i.ctx = i.getContext("2d");
            i.ctx.drawImage(img, 0, 0);
            return i;
        },
        rgb2hsl(r,g,b){ // integers in the range 0-255
            var min, max, dif, h, l, s;
            h = l = s = 0;
            r /= 255;  // normalize channels
            g /= 255;
            b /= 255;
            min = Math.min(r, g, b);
            max = Math.max(r, g, b);
            if(min === max){  // no colour so early exit
                return {
                    h, s,
                    l : Math.floor(min * 100),  // Note there is loss in this conversion
                }
            }
            dif = max - min;
            l = (max + min) / 2;
            if (l > 0.5) { s = dif / (2 - max - min) }
            else { s = dif / (max + min) }
            if (max === r) {
                if (g < b) { h = (g - b) / dif + 6.0 }
                else { h = (g - b) / dif }                   
            } else if(max === g) { h = (b - r) / dif + 2.0 }
            else {h = (r - g) / dif + 4.0 }   
            h = Math.floor(h * 60);
            s = Math.floor(s * 100);
            l = Math.floor(l * 100);
            return {h, s, l};
        },
        hsl2rgb (h, s, l) { // h in range integer 0-360 (cyclic) and s,l 0-100 both integers
            var p, q;
            const hue2Channel = (h) => {
                h = h < 0.0 ? h + 1 : h > 1 ? h - 1 : h;
                if (h < 1 / 6) { return p + (q - p) * 6 * h }
                if (h < 1 / 2) { return q }
                if (h < 2 / 3) { return p + (q - p) * (2 / 3 - h) * 6 }
                return p;        
            }
            s = Math.floor(s)/100;
            l = Math.floor(l)/100;
            if (s <= 0){  // no colour
                return {
                    r : Math.floor(l * 255),
                    g : Math.floor(l * 255),
                    b : Math.floor(l * 255),
                }
            }
            h = (((Math.floor(h) % 360) + 360) % 360) / 360; // normalize
            if (l < 1 / 2) { q = l * (1 + s) } 
            else { q = l + s - l * s }
            p = 2 * l - q;        
            return {
                r : Math.floor(hue2Channel(h + 1 / 3) * 255),
                g : Math.floor(hue2Channel(h)         * 255),
                b : Math.floor(hue2Channel(h - 1 / 3) * 255),
            }    
            
        },        
        rgba2Hex4(r,g,b,a=255){
            if(typeof r === "object"){
                g = r.g;
                b = r.b;
                a = r.a !== undefined ? r.a : a;
                r = r.r;
            }
            return `#${toHex(r)}${toHex(g)}${toHex(b)}${toHex(a)}`; 
        },
        hex2RGBA(hex){ // Not CSS colour as can have extra 2 or 1 chars for alpha
                                  // #FFFF & #FFFFFFFF last F and FF are the alpha range 0-F & 00-FF
            if(typeof hex === "string"){
                var str = "rgba(";
                if(hex.length === 4 || hex.length === 5){
                    str += (parseInt(hex.substr(1,1),16) * 16) + ",";
                    str += (parseInt(hex.substr(2,1),16) * 16) + ",";
                    str += (parseInt(hex.substr(3,1),16) * 16) + ",";
                    if(hex.length === 5){
                        str += (parseInt(hex.substr(4,1),16) / 16);
                    }else{
                        str += "1";
                    }
                    return str + ")";
                }
                if(hex.length === 7 || hex.length === 9){
                    str += parseInt(hex.substr(1,2),16) + ",";
                    str += parseInt(hex.substr(3,2),16) + ",";
                    str += parseInt(hex.substr(5,2),16) + ",";
                    if(hex.length === 9){
                        str += (parseInt(hex.substr(7,2),16) / 255).toFixed(3);
                    }else{
                        str += "1";
                    }
                    return str + ")";                
                }
                return "rgba(0,0,0,0)";
            }
            
                
        },            
        createGradient(ctx, type, x, y, xx, yy, colours){ // Colours MUST be array of hex colours NOT CSS colours
                                                          // See this.hex2RGBA for details of format
            var i,g,c;
            var len = colours.length;
            if(type.toLowerCase() === "linear"){
                g = ctx.createLinearGradient(x,y,xx,yy);
            }else{
                g = ctx.createRadialGradient(x,y,xx,x,y,yy);
            }
            for(i = 0; i < len; i++){
                c = colours[i];
                if(typeof c === "string"){
                    if(c[0] === "#"){
                        c = this.hex2RGBA(c);
                    }
                    g.addColorStop(Math.min(1,i / (len -1)),c); // need to clamp top to 1 due to floating point errors causes addColorStop to throw rangeError when number over 1
                }
            }
            return g;
        },
        padImage(img,amount){
            var image = this.canvas(img.width + amount * 2, img.height + amount * 2);
            image.ctx = image.getContext("2d");
            image.ctx.drawImage(img, amount, amount);
            return image;
        },
        getImageData(image, w = image.width, h = image.height) {  // cut down version to prevent intergration 
            if(image.ctx && image.ctx.imageData){
                return image.ctx.imageData;
            }
            return (image.ctx || (this.image2Canvas(image).ctx)).getImageData(0, 0, w, h);
        },
    };
    return tools;
})();

/** ImageTools.js end **/

Ответ 2

Звучит как интересная проблема!

Каждый алгоритм, который вы используете для генерации цветов, вероятно, имеет предвзятость к определенным цветам в их соответствующих случайных цветовых алгоритмах.

То, что вы, вероятно, видите, является конечным результатом этого смещения для каждого. Оба выбирают более темные и светлые цвета независимо друг от друга.

Возможно, имеет смысл сохранить хэш общих цветов и использовать этот хеш в отличие от использования случайно сгенерированных цветов.

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

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

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

Или вместо того, чтобы принимать 6 наиболее часто встречающихся цветов, почему бы не взять общий градиент цвета и не попробовать?

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

Использование наиболее часто встречающегося цвета на изображении, похоже, работает очень хорошо. как показано в моем примере ниже. Это аналогичный метод, который Blindman67 использует без массивного вздутия библиотек и выполнения ненужных шагов, я использовал те же изображения, которые Blindman67 использует для справедливого сравнения набора результатов.

Смотрите Получите средний цвет изображения через Javascript для получения среднего цвета (getAverageRGB()функция, написанная James).

var images = [
  "https://upload.wikimedia.org/wikipedia/commons/thumb/2/22/Cistothorus_palustris_CT.jpg/450px-Cistothorus_palustris_CT.jpg",
  "https://upload.wikimedia.org/wikipedia/commons/thumb/3/37/Black-necked_Stilt_%28Himantopus_mexicanus%29%2C_Corte_Madera.jpg/362px-Black-necked_Stilt_%28Himantopus_mexicanus%29%2C_Corte_Madera.jpg",
  "https://upload.wikimedia.org/wikipedia/commons/thumb/c/cc/Daurian_redstart_at_Daisen_Park_in_Osaka%2C_January_2016.jpg/573px-Daurian_redstart_at_Daisen_Park_in_Osaka%2C_January_2016.jpg",
  "https://upload.wikimedia.org/wikipedia/commons/thumb/d/da/Myioborus_torquatus_Santa_Elena.JPG/675px-Myioborus_torquatus_Santa_Elena.JPG",
  "https://upload.wikimedia.org/wikipedia/commons/thumb/e/ef/Great_tit_side-on.jpg/645px-Great_tit_side-on.jpg",
  "https://upload.wikimedia.org/wikipedia/commons/thumb/5/55/Sarcoramphus_papa_%28K%C3%B6nigsgeier_-_King_Vulture%29_-_Weltvogelpark_Walsrode_2013-01.jpg/675px-Sarcoramphus_papa_%28K%C3%B6nigsgeier_-_King_Vulture%29_-_Weltvogelpark_Walsrode_2013-01.jpg",
];

// append images
for (var i = 0; i < images.length; i++) {
  var img = document.createElement('img'),
div = document.createElement('div');
  img.crossOrigin = "Anonymous";

  img.style.border = '1px solid black';
  img.style.margin = '5px';

  div.appendChild(img);

  document.body.appendChild(div);
  (function(img, div) {
img.addEventListener('load', function() {
  var avg = getAverageRGB(img);
  div.style = 'background: rgb(' + avg.r + ',' + avg.g + ',' + avg.b + ')';
  img.style.height = '128px';
  img.style.width = '128px';
});
img.src = images[i];
  }(img, div));
}

function getAverageRGB(imgEl) { // not my work, see http://jsfiddle.net/xLF38/818/
  var blockSize = 5, // only visit every 5 pixels
defaultRGB = {
  r: 0,
  g: 0,
  b: 0
}, // for non-supporting envs
canvas = document.createElement('canvas'),
context = canvas.getContext && canvas.getContext('2d'),
data, width, height,
i = -4,
length,
rgb = {
  r: 0,
  g: 0,
  b: 0
},
count = 0;

  if (!context) {
return defaultRGB;
  }

  height = canvas.height = imgEl.offsetHeight || imgEl.height;
  width = canvas.width = imgEl.offsetWidth || imgEl.width;

  context.drawImage(imgEl, 0, 0);
  try {
data = context.getImageData(0, 0, width, height);
  } catch (e) {
return defaultRGB;
  }

  length = data.data.length;

  while ((i += blockSize * 4) < length) {
++count;
rgb.r += data.data[i];
rgb.g += data.data[i + 1];
rgb.b += data.data[i + 2];
  }

  // ~~ used to floor values
  rgb.r = ~~(rgb.r / count);
  rgb.g = ~~(rgb.g / count);
  rgb.b = ~~(rgb.b / count);

  return rgb;
}

Ответ 3

Это зависит от того, где текст накладывается на фоновое изображение. Если фон имеет некоторую большую особенность на части, текст, скорее всего, будет удален от него, поэтому он должен контрастировать с той частью изображения, но вы также можете выбрать определенный цвет или добавить другие цвета в образ. Я думаю, что практически говоря, вам нужно будет создать виджет для людей, которые легко могут перемещать/корректировать цвет переднего плана в интерактивном режиме. Или вам нужно будет создать систему глубокого обучения, чтобы сделать это действительно эффективно.