Onde está o Blackhat?

27

Desafio

Escreva um código que, dada a imagem de um painel de uma história em quadrinhos aleatória do xkcd, retorne um valor verdadeiro se Blackhat estiver na história em quadrinhos ou falsey, se não.

Quem é Blackhat?

Blackhat é o nome não oficial dado ao personagem dos quadrinhos xkcd que usa um chapéu preto:

Retirado da página Explain xkcd no Blackhat

O chapéu de Blackhat é sempre reto, preto e tem a mesma aparência da imagem acima.

Outros personagens também podem ter chapéus e cabelos, mas nenhum terá chapéus pretos e retos.

Entrada

A imagem pode ser inserida da maneira que você desejar, seja um caminho para a imagem ou bytes via STDIN. Você não precisa usar um URL como entrada.

Regras

A resposta codificada não é proibida, mas não é apreciada.

Você não tem permissão para acessar a Internet para obter a resposta.

Exemplos

Todas as imagens colhidas a partir de imagens de https://xkcd.com

Blackhat está no painel (retorno truthy)


Blackhat não está no painel (retorno falsey)


Bateria de teste

As 20 imagens que contêm Blackhat podem ser encontradas aqui: https://beta-decay.github.io/blackhat.zip

As 20 imagens que não contêm Blackhat podem ser encontradas aqui: https://beta-decay.github.io/no_blackhat.zip

Se você quiser que mais imagens testem seus programas (para treinar para os casos de mistério), você pode encontrar uma lista de todas as aparências do Blackhat aqui: http://www.explainxkcd.com/wiki/index.php/ Comics_featuring_Black_Hat

Ganhando

O programa que identifica corretamente se o Blackhat está nos quadrinhos ou não para a maioria das imagens vence. Seu cabeçalho deve incluir sua pontuação como uma porcentagem.

No caso de um tiebreak, os programas vinculados receberão imagens "misteriosas" (ou seja, aquelas que apenas eu conheço). O código que identifica o mais corretamente vence o desempate.

As imagens misteriosas serão reveladas juntamente com as pontuações.

Nota: parece que o nome de Randall para ele pode ser Hat Guy. Eu prefiro Blackhat embora.

Beta Decay
fonte
12
Não ficarei surpreso se o Mathematica tiver um built-in para isso. ( Para referência )
J. Sallé
5
Sugestão para um desempate diferente: tenha um conjunto de imagens diferente e menor (digamos 5 casos verdadeiros e 5 falsos) que não são revelados aqui, e o vencedor do desempate é o que generaliza melhor para essas imagens desconhecidas. Isso incentivaria as soluções mais genéricas e inteligentes, que se adaptam a essas imagens específicas.
sundar - Restabelece Monica
3
Os casos de teste com a polícia e com a RIAA / MPAA são apenas maus. Boa bateria de teste, @BetaDecay.
sundar - Restabelece Monica
1
Vamos continuar esta discussão no chat .
Beta Decay
1
@ Night2 Desculpe! Eu só planejava fazer com que houvesse empate. Bom trabalho em 100%!
Decay Beta

Respostas:

16

PHP (> = 7), 100% (40/40)

<?php

set_time_limit(0);

class BlackHat
{
    const ROTATION_RANGE = 45;

    private $image;
    private $currentImage;
    private $currentImageWidth;
    private $currentImageHeight;

    public function __construct($path)
    {
        $this->image = imagecreatefrompng($path);
    }

    public function hasBlackHat()
    {
        $angles = [0];

        for ($i = 1; $i <= self::ROTATION_RANGE; $i++) {
            $angles[] = $i;
            $angles[] = -$i;
        }

        foreach ($angles as $angle) {
            if ($angle == 0) {
                $this->currentImage = $this->image;
            } else {
                $this->currentImage = $this->rotate($angle);
            }

            $this->currentImageWidth = imagesx($this->currentImage);
            $this->currentImageHeight = imagesy($this->currentImage);

            if ($this->findBlackHat()) return true;
        }

        return false;
    }

    private function findBlackHat()
    {
        for ($y = 0; $y < $this->currentImageHeight; $y++) {
            for ($x = 0; $x < $this->currentImageWidth; $x++) {
                if ($this->isBlackish($x, $y) && $this->isHat($x, $y)) return true;
            }
        }

        return false;
    }

    private function isHat($x, $y)
    {
        $hatWidth = $this->getBlackishSequenceSize($x, $y, 'right');
        if ($hatWidth < 10) return false;

        $hatHeight = $this->getBlackishSequenceSize($x, $y, 'bottom');

        $hatLeftRim = $hatRightRim = 0;
        for (; ; $hatHeight--) {
            if ($hatHeight < 5) return false;

            $hatLeftRim = $this->getBlackishSequenceSize($x, $y + $hatHeight, 'left');
            if ($hatLeftRim < 3) continue;

            $hatRightRim = $this->getBlackishSequenceSize($x + $hatWidth, $y + $hatHeight, 'right');
            if ($hatRightRim < 2) $hatRightRim = $this->getBlackishSequenceSize($x + $hatWidth, $y + $hatHeight, 'right', 'isLessBlackish');
            if ($hatRightRim < 2) continue;

            break;
        }

        $ratio = $hatWidth / $hatHeight;
        if ($ratio < 2 || $ratio > 4.2) return false;

        $widthRatio = $hatWidth / ($hatLeftRim + $hatRightRim);
        if ($widthRatio < 0.83) return false;
        if ($hatHeight / $hatLeftRim < 1 || $hatHeight / $hatRightRim < 1) return false;

        $pointsScore = 0;
        if ($this->isSurroundedBy($x, $y, 3, true, true, false, false)) $pointsScore++;
        if ($this->isSurroundedBy($x + $hatWidth, $y, 3, true, false, false, true)) $pointsScore++;
        if ($this->isSurroundedBy($x, $y + $hatHeight, 3, false, false, true, false)) $pointsScore++;
        if ($this->isSurroundedBy($x + $hatWidth, $y + $hatHeight, 3, false, false, true, false)) $pointsScore++;
        if ($this->isSurroundedBy($x - $hatLeftRim, $y + $hatHeight, 3, true, true, true, false)) $pointsScore++;
        if ($this->isSurroundedBy($x + $hatWidth + $hatRightRim, $y + $hatHeight, 3, true, false, true, true)) $pointsScore++;
        if ($pointsScore < 3 || ($hatHeight >= 19 && $pointsScore < 4) || ($hatHeight >= 28 && $pointsScore < 5)) return false;

        $middleCheckSize = ($hatHeight >= 15 ? 3 : 2);
        if (!$this->isSurroundedBy($x + (int)($hatWidth / 2), $y, $middleCheckSize, true, null, null, null)) return false;
        if (!$this->isSurroundedBy($x + (int)($hatWidth / 2), $y + $hatHeight, $middleCheckSize, null, null, true, null)) {
            if (!$this->isSurroundedBy($x + (int)(($hatWidth / 4) * 3), $y + $hatHeight, $middleCheckSize, null, null, true, null)) return false;
        }
        if (!$this->isSurroundedBy($x, $y + (int)($hatHeight / 2), $middleCheckSize + 1, null, true, null, null)) return false;
        if (!$this->isSurroundedBy($x + $hatWidth, $y + (int)($hatHeight / 2), $middleCheckSize, null, null, null, true)) return false;

        $badBlacks = 0;
        for ($i = 1; $i <= 3; $i++) {
            if ($y - $i >= 0) {
                if ($this->isBlackish($x, $y - $i)) $badBlacks++;
            }

            if ($x - $i >= 0 && $y - $i >= 0) {
                if ($this->isBlackish($x - $i, $y - $i)) $badBlacks++;
            }
        }
        if ($badBlacks > 2) return false;

        $total = ($hatWidth + 1) * ($hatHeight + 1);
        $blacks = 0;
        for ($i = $x; $i <= $x + $hatWidth; $i++) {
            for ($j = $y; $j <= $y + $hatHeight; $j++) {
                $isBlack = $this->isBlackish($i, $j);
                if ($isBlack) $blacks++;
            }
        }

        if (($total / $blacks > 1.15)) return false;

        return true;
    }

    private function getColor($x, $y)
    {
        return imagecolorsforindex($this->currentImage, imagecolorat($this->currentImage, $x, $y));
    }

    private function isBlackish($x, $y)
    {
        $color = $this->getColor($x, $y);
        return ($color['red'] < 78 && $color['green'] < 78 && $color['blue'] < 78 && $color['alpha'] < 30);
    }

    private function isLessBlackish($x, $y)
    {
        $color = $this->getColor($x, $y);
        return ($color['red'] < 96 && $color['green'] < 96 && $color['blue'] < 96 && $color['alpha'] < 40);
    }

    private function getBlackishSequenceSize($x, $y, $direction, $fn = 'isBlackish')
    {
        $size = 0;

        if ($direction == 'right') {
            for ($x++; ; $x++) {
                if ($x >= $this->currentImageWidth) break;
                if (!$this->$fn($x, $y)) break;
                $size++;
            }
        } elseif ($direction == 'left') {
            for ($x--; ; $x--) {
                if ($x < 0) break;
                if (!$this->$fn($x, $y)) break;
                $size++;
            }
        } elseif ($direction == 'bottom') {
            for ($y++; ; $y++) {
                if ($y >= $this->currentImageHeight) break;
                if (!$this->$fn($x, $y)) break;
                $size++;
            }
        }

        return $size;
    }

    private function isSurroundedBy($x, $y, $size, $top = null, $left = null, $bottom = null, $right = null)
    {
        if ($top !== null) {
            $flag = false;
            for ($i = 1; $i <= $size; $i++) {
                if ($y - $i < 0) break;
                $isBlackish = $this->isBlackish($x, $y - $i);

                if (
                    ($top && !$isBlackish) ||
                    (!$top && $isBlackish)
                ) {
                    $flag = true;
                } elseif ($flag) {
                    return false;
                }
            }
            if (!$flag) return false;
        }

        if ($left !== null) {
            $flag = false;
            for ($i = 1; $i <= $size; $i++) {
                if ($x - $i < 0) break;
                $isBlackish = $this->isBlackish($x - $i, $y);

                if (
                    ($left && !$isBlackish) ||
                    (!$left && $isBlackish)
                ) {
                    $flag = true;
                } elseif ($flag) {
                    return false;
                }
            }
            if (!$flag) return false;
        }

        if ($bottom !== null) {
            $flag = false;
            for ($i = 1; $i <= $size; $i++) {
                if ($y + $i >= $this->currentImageHeight) break;
                $isBlackish = $this->isBlackish($x, $y + $i);

                if (
                    ($bottom && !$isBlackish) ||
                    (!$bottom && $isBlackish)
                ) {
                    $flag = true;
                } elseif ($flag) {
                    return false;
                }
            }
            if (!$flag) return false;
        }

        if ($right !== null) {
            $flag = false;
            for ($i = 1; $i <= $size; $i++) {
                if ($x + $i >= $this->currentImageWidth) break;
                $isBlackish = $this->isBlackish($x + $i, $y);

                if (
                    ($right && !$isBlackish) ||
                    (!$right && $isBlackish)
                ) {
                    $flag = true;
                } elseif ($flag) {
                    return false;
                }
            }
            if (!$flag) return false;
        }

        return true;
    }

    private function rotate($angle)
    {
        return imagerotate($this->image, $angle, imagecolorallocate($this->image, 255, 255, 255));
    }
}

$bh = new BlackHat($argv[1]);
echo $bh->hasBlackHat() ? 'true' : 'false';

Para executá-lo:

php <filename> <image_path>

Exemplo:

php black_hat.php "/tmp/blackhat/1.PNG"

Notas

  • Imprime "true" se encontrar chapéu preto e "false" se não encontrar.
  • Isso deve funcionar também nas versões anteriores do PHP, mas, para ser seguro, use PHP> = 7 no GD .
  • Na verdade, esse script tenta encontrar o chapéu e, ao fazer isso, pode girar a imagem muitas vezes e sempre verifica milhares e milhares de pixels e pistas. Portanto, quanto maior a imagem ou mais pixels escuros, o script levará mais tempo para concluir. Porém, deve demorar alguns segundos a um minuto para a maioria das imagens.
  • Eu adoraria treinar mais esse script, mas não tenho tempo suficiente para fazê-lo.
  • Este script não é jogado de golfe (novamente porque não tenho tempo suficiente), mas tem muito potencial para jogar golfe em caso de empate.

Alguns exemplos de chapéus pretos detectados:

insira a descrição da imagem aqui

Esses exemplos são adquiridos desenhando linhas vermelhas em pontos especiais encontrados na imagem que o script decidiu ter um chapéu preto (as imagens podem ter rotação comparadas às originais).


Extra

Antes de postar aqui, testei esse script em outro conjunto de 15 imagens, 10 com chapéu preto e 5 sem chapéu preto e ele também foi correto para todas elas (100%).

Aqui está o arquivo ZIP contendo imagens de teste extras que usei: extra.zip

No extra/blackhatdiretório, os resultados da detecção com linhas vermelhas também estão disponíveis. Por exemplo, extra/blackhat/1.pngé a imagem de teste e extra/blackhat/1_r.pngé o resultado da detecção.

Night2
fonte
O tiebreak não é código de golfe. Em vez disso, os programas são alimentados com casos de teste ocultos até que o desempate seja resolvido. Em seguida, mostrarei o resultado e publicarei os casos de teste :)
Beta Decay
1
@BetaDecay: Obrigado pelo esclarecimento, esta frase (vitórias mais curtas em um empate) estava na minha cabeça nas versões anteriores da pergunta, então eu estava pensando que, se um empate acontecer em casos de teste ocultos, o código mais curto vence. Minha culpa!
night2
7
Você ganha o prêmio para menos linguagem de processamento de imagem provavelmente também :)
Anush
@Anush Bem, pelo menos PHP tem imagerotatebuilt-in, então ...
user202729
O que eu mais gosto no PHP é que ele tem algumas funcionalidades básicas para quase tudo. Ele inclui o GD por muitos anos e o GD realmente satisfaz as necessidades mais comuns de trabalhar com imagens. Mas o que eu mais gosto no PHP é que sempre existem algumas extensões / pacotes que oferecem mais (por ter uma comunidade enorme). Por exemplo, existem extensões OpenCV para PHP que permitem o processamento real de imagens!
night2
8

Matlab, 87,5%

function hat=is_blackhat_here2(filepath)

img_hsv = rgb2hsv(imread(filepath));
img_v = img_hsv(:,:,3);

bw = imdilate(imerode( ~im2bw(img_v), strel('disk', 4, 8)), strel('disk', 4, 8));
bw = bwlabel(bw, 8);
bw = imdilate(imerode(bw, strel('disk', 1, 4)), strel('disk', 1, 4));
bw = bwlabel(bw, 4);

region_stats = regionprops(logical(bw), 'all');
hat = false;
for i = 1 : numel(region_stats)
    if mean(img_v(region_stats(i).PixelIdxList)) < 0.15 ...
            && region_stats(i).Area > 30 ...
            && region_stats(i).Solidity > 0.88 ...
            && region_stats(i).Eccentricity > 0.6 ...
            && region_stats(i).Eccentricity < 1 ...
            && abs(region_stats(i).Orientation) < 75...
            && region_stats(i).MinorAxisLength / region_stats(i).MajorAxisLength < 0.5;
        hat = true;
        break;
    end
end

Aprimoramento da versão anterior, com algumas verificações adicionadas na forma das regiões candidatas.

Erros de classificação no conjunto HAT : imagens 4, 14, 15, 17 .

Erros de classificação no conjunto NÃO HAT : imagens 4 .

Alguns exemplos de imagens classificadas corrigidas: insira a descrição da imagem aqui insira a descrição da imagem aqui

Exemplo de uma imagem classificada incorreta:

insira a descrição da imagem aqui

VERSÃO ANTIGA (77,5%)

function hat=is_blackhat_here(filepath)

img_hsv = rgb2hsv(imread(filepath));
img_v = img_hsv(:,:,3);
bw = imerode(~im2bw(img_v), strel('disk', 5, 8));

hat =  mean(img_v(bw)) < 0.04;

Abordagem baseada na erosão da imagem, semelhante à solução proposta pela Mnemonic, mas baseada no canal V da imagem HSV. Além disso, o valor médio do canal da área selecionada é verificado (não seu tamanho).

Erros de classificação no conjunto HAT : imagens 4, 5, 10 .

Erros de classificação no conjunto NÃO HAT : imagens 4, 5, 6, 7, 13, 14 .

PieCot
fonte
7

Pitão , 62,5%

<214.O.n'z

Aceita o nome do arquivo de um arquivo de imagem no stdin. Retorna Truese a média de todos os seus componentes de cores RGB for maior que 214. Você leu certo: aparentemente as imagens blackhat tendem a ser mais brilhantes que as imagens blackhat.

(Certamente alguém pode fazer melhor - isso não é !)

Anders Kaseorg
fonte
2
Fiquei impressionado com o poder de Pyth até que percebi: D
Beta Decay
Por um instante, pensei "Desde quando Pyth se incorporou ao reconhecimento de imagens de blackhat"
Luis felipe De jesus Munoz
2
Eu=2540.(40.Eu)240.7,7%
user202729
6

Python 2, 65% 72,5% 77,5% (= 31/40)

import cv2
import numpy as np
from scipy import misc

def blackhat(path):
    im = misc.imread(path)
    black = (im[:, :, 0] < 10) & (im[:, :, 1] < 10) & (im[:, :, 2] < 10)
    black = black.astype(np.ubyte)

    black = cv2.erode(black, np.ones((3, 3)), iterations=3)

    return 5 < np.sum(black) < 2000

Isso descobre quais pixels são pretos e depois elimina pequenos pedaços contíguos. Certamente espaço para melhorias aqui.

Zacharý
fonte