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';
To run it:
php <filename> <image_path>
Example:
php black_hat.php "/tmp/blackhat/1.PNG"
Notes
- Prints "true" if finds black hat and "false" if doesn't find it.
- This should work on previous versions of PHP as well, but to be safe, use PHP>=7 with GD.
- This script actually tries to find the hat and by doing so, it might rotate the image many many times and each time checks for thousands and thousands of pixels and clues. So the larger the image is or the more dark pixels it has, the script will take more time to finish. It should take a few seconds to a minute for majority of images though.
- I would love to train this script more, but I don't have enough time to do so.
- This script is not golfed (again because I don't have enough time), but has lots of potential to golf in case of a tie.
Some examples of detected black hats:
These examples are acquired by drawing red lines on special points found on the image that script decided has a black hat (images can have rotation compared to original ones).
Extra
Before posting here, I did test this script against another set of 15 images, 10 with black hat and 5 without black hat and it went correct for all of them as well (100%).
Here is the ZIP file containing extra test images I used: extra.zip
In the extra/blackhat
directory, the detection results with red lines are also available. For example extra/blackhat/1.png
is the test image and extra/blackhat/1_r.png
is the detection result of it.
Major stumbling blocks in no_blackhat.zip: #10 the pilot; #19 the webcams – Jonathan Allan – 2018-07-23T16:39:09.527
@JonathanAllan Similarly in blackhat.zip: #6 two hat black hat and #16 blackhat with a bit of hair – Beta Decay – 2018-07-23T16:42:15.363
12
I'll not be surprised if Mathematica has a built-in for that. (For reference)
– J. Sallé – 2018-07-23T17:04:56.5175Suggestion for a different tie breaker: have a different, smaller set of images (say 5 true cases and 5 false) that are unrevealed here, and the winner of the tie breaker is the one that generalises best to these unknown images. That would incentivize the more generic smarter solutions vs ones that overfit to these specific images. – sundar - Reinstate Monica – 2018-07-23T22:10:16.787
3The test cases with the police and with the RIAA/MPAA are just evil. Good test battery, @BetaDecay. – sundar - Reinstate Monica – 2018-07-24T18:09:52.923
1
Let us continue this discussion in chat.
– Beta Decay – 2018-07-25T20:48:03.963Relevant: "How do I find Waldo with Mathematica", which implements small-area-of-red-and-white-stripes detection in an arbitrary image https://stackoverflow.com/questions/8479058/how-do-i-find-waldo-with-mathematica
– Sparr – 2018-07-26T02:26:42.343@BetaDecay: Thanks for the bounty! I am curious to see your hidden/mystery images and test my script with them too (but never mind if you haven't created that set yet). – Night2 – 2018-07-30T17:00:37.183
1@Night2 Sorry! I only planned to make any of there was tie. Nice work on 100% though! – Beta Decay – 2018-07-30T17:52:44.140