2018年11月29日 星期四

「智泉拾叁」創作


2018 年 11 月 22 日,「智泉 13」正式開始。距離上一次的體驗式課程,已經相隔有 17 年左右。如同 19 年前參與「IN117」一樣,我也一起設計團隊標誌及製服;同時也發揮想像力,設計一些美術作品。其中一樣作品,是希望用元祖太極圖案,加上中文字「拾叁」來創作。如果一個一個地畫出太極實在很耗時,作為程式員,這個情景可以幫得上忙。


要製作出這樣的效果,首先要準備一張遮罩圖。黑色代表繪畫太極的空間,白色代表留白的地方。把遮罩圖黑色地方記下,然後在這些地方隨機生成太極的圖案。我希望把整個過程以動畫方式呈現,所以加入 Circle 類別用來處理太極由小變大的過程;並且把每一步驟的幀記錄下來。最後,以 ffmpeg 指令「ffmpeg -framerate 60 -i out_%04d.png -s:v 1024x550 -c:v libx264 -profile:v high -crf 20 -pix_fmt yuv420p w13.mp4」生成影片。
<?php
//----------------------------------------------------------------------------------------
//  Packing Circle with Mask Image
//----------------------------------------------------------------------------------------
//  Platform: macOS Mojave + PHP5
//  Written by Pacess HO
//  Copyrights Pacess Studio, 2018.  All rights reserved.
//----------------------------------------------------------------------------------------

class Circle  {
   private $isGrowing = true;

   public $x = 0;
   public $y = 0;
   public $r = 3.0;

   //----------------------------------------------------------------------------------------
   function setup($x, $y)  {
      $this->x = $x;
      $this->y = $y;
      $this->r = 3.0;
   }

   //----------------------------------------------------------------------------------------
   function draw($image)  {
      $foreground = imagecolorallocate($image, 0, 0, 0);
      imageellipse($image, $this->x, $this->y, $this->r*2, $this->r*2, $foreground);
   }

   //----------------------------------------------------------------------------------------
   function grow()  {
      if ($this->isGrowing == false)  {return;}
      $this->r++;

      if ($this->r > 30)  {$this->isGrowing = false;}
   }

   //----------------------------------------------------------------------------------------
   function stopGrow()  {$this->isGrowing = false;}

   //----------------------------------------------------------------------------------------
   function isEdge($width, $height)  {
      if (($this->x-$this->r) < 0)  {return true;}
      if (($this->y-$this->r) < 0)  {return true;}
      if (($this->x+$this->r) > $width)  {return true;}
      if (($this->y+$this->r) > $height)  {return true;}
      return false;
   }
}

//========================================================================================
//  Main program
$_width = 1024;
$_height = 768;

//  Loading mask image
list($_width, $_height) = getimagesize("w13.png");
$backgroundImage = imagecreatefrompng("w13.png");

//  Convert mask into spot array
$spotArray = array();
for ($y=0; $y<$_height; $y++)  {
   for ($x=0; $x<$_width; $x++)  {
      $color = imagecolorat($backgroundImage, $x, $y);
      $blue = $color&255;
      if ($blue >= 80)  {continue;}

      $spotArray[] = array($x, $y);
   }
}

//----------------------------------------------------------------------------------------
//  Create logo animation
$max = 99999;
$_array = array();
for ($i=0; $i<1000; $i++)  {

   //  New circle
   for ($j=0; $j<5; $j++)  {

      $valid = true;
      $value = rand(0, count($spotArray));
      $spot = $spotArray[$value];
      $x = $spot[0];
      $y = $spot[1];

      $newCircle = new Circle();
      $newCircle->setup($x, $y);
      foreach ($_array as $circle)  {

         $distance = sqrt(pow($circle->x-$newCircle->x, 2)+pow($circle->y-$newCircle->y, 2));
         if ($distance < ($circle->r+$newCircle->r+2))  {$valid = false;}
      }

      if ($valid == true && $max > 0)  {
         $_array[] = $newCircle;
         $max--;
      }
   }

   //  Create image
   $image = imagecreatetruecolor($_width, $_height);
   $foreground = imagecolorallocate($image, 0, 0, 0);
   $background = imagecolorallocate($image, 255, 255, 255);
   imagefilledrectangle($image, 0, 0, $_width, $_height, $background);
   foreach ($_array as $circle)  {

      $boolean = $circle->isEdge($_width, $_height);
      if ($boolean == true)  {$circle->stopGrow();}

      $x = $circle->x;
      $y = $circle->y;
      $r = $circle->r;
      imageellipse($image, $x, $y, $r*2, $r*2, $foreground);

      //  Overlapping
      $overlapping = false;
      foreach ($_array as $circle2)  {

         if ($circle == $circle2)  {continue;}
         $distance = sqrt(pow($circle->x-$circle2->x, 2)+pow($circle->y-$circle2->y, 2));
         if ($distance < ($circle->r+$circle2->r+2))  {$circle->stopGrow();}
      }

      $circle->grow();
   }

   $filename = sprintf("out_%04d.png", $i);
   imagepng($image, $filename);
   imagedestroy($image);
}
?>

2018年11月10日 星期六

準備 WhatsApp 貼紙格式


最近 WhatsApp 推出貼紙功能,特別之處是貼紙不是由內部的商店下載,而是用外部 App 加入。WhatsApp 提供了參考程式,方便大眾自行加入貼紙。不過,貼紙必須為 512x512 像素 PNG 或 WEBP 格式。

我有一些貼紙不是這個格式,於是編寫了 PHP 程式做準備工作:
<?php
//----------------------------------------------------------------------------------------
//  Create a square base transparent image
//----------------------------------------------------------------------------------------
//  Platform: macOS Mojave + PHP
//  Written by Pacess HO
//  Copyright Pacess Studio, 2018.  All rights reserved.
//----------------------------------------------------------------------------------------

$width = 512;
$height = 512;

//----------------------------------------------------------------------------------------
$fileArray = scandir("./");
foreach ($fileArray as $filename)  {

   //  Skip directories
   if ($filename == ".")  {continue;}
   if ($filename == "..")  {continue;}

   $index = strpos($filename, ".png");
   if ($index == false)  {continue;}

   //  This is a PNG file, create output image
   echo("Processing $filename...\n");
   $outputImage = imagecreatetruecolor($width, $height);
   imagealphablending($outputImage, false);
   $color = imagecolorallocatealpha($outputImage, 255, 255, 255, 127);
   imagefilledrectangle($outputImage, 0, 0, $width, $height, $color);
   imagealphablending($outputImage, true);

   //  Get image size
   $size = getimagesize($filename);
   $imageWidth = $size[0];
   $imageHeight = $size[1];

   //  Calculate zoom scale
   $scaleW = $width/$imageWidth;
   $scaleH = $height/$imageHeight;

   $scale = $scaleW;
   if ($scaleW > $scaleH)  {$scale = $scaleH;}
   $zoomWidth = intval($imageWidth*$scale);
   $zoomHeight = intval($imageHeight*$scale);

   //  Put image to output image
   $x = intval(($width-$zoomWidth)/2);
   $y = intval(($height-$zoomHeight)/2);

   $stickerImage = imagecreatefrompng($filename);
   if ($stickerImage == null)  {continue;}
   imagecopyresampled($outputImage, $stickerImage, $x, $y, 0, 0, $zoomWidth, $zoomHeight, $imageWidth, $imageHeight);

   imagealphablending($outputImage, false);
   imagesavealpha($outputImage, true);
   imagepng($outputImage, "_".$filename);
   imagedestroy($outputImage);
}
?>

2018年11月5日 星期一

用 Python 修正相片日期


上星期參加了 WTIA 舉辦的「2018 北台灣物聯網投資合作商機考察參訪團」。帶了相機及手機拍攝活動相片。回到香港,才發現相機在 8 月到名古屋時調快了一小時,於是編寫以下 Python 程式,讀取相片中的 EXIF 資料,把所有 Canon 拍攝的相片日期都調慢一小時,變回正確時間。
##----------------------------------------------------------------------------------------
##  Fix Photo Creation Date
##----------------------------------------------------------------------------------------
##  Platform: macOS Mojave + Python 2.7
##  Copyrights Pacess Studio, 2018.  All rights reserved.
##----------------------------------------------------------------------------------------

import os
import time
import exifread

##----------------------------------------------------------------------------------------
##  Global variables
_path = "./"

##----------------------------------------------------------------------------------------
##  Get files from directory
for root, dirs, files in os.walk(_path):

   for file in files:
   
      if file.startswith("."):
         continue

      if not file.endswith(".JPG"):
         continue

      print("\nProcessing "+file+"...", end="")

      ##----------------------------------------------------------------------------------------
      ##  Get EXIF
      handle = open(_path+file, "rb")
      tags = exifread.process_file(handle)
   
      machine = str(tags["Image Make"])
      print(machine, end="")

      ##  Process only if "Canon"
      if "Canon" not in machine:
         continue

      ##----------------------------------------------------------------------------------------
      ##  Subtract one hour
      #datetime = os.path.getmtime(file)
      #timeString = time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(datetime))
      timeString = str(tags["Image DateTime"])
      datetime = time.mktime(time.strptime(timeString, "%Y:%m:%d %H:%M:%S"))

      newDatetime = datetime-(60*60)
      newTimeString = time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(newDatetime))
      
      os.utime(_path+file, (newDatetime, newDatetime))
      print(" ("+timeString+" => GMT:"+newTimeString+")", end="")

print("\nDone\n")