CakePHPでApache Solrに接続したかったら、datasourceを書いてみた
CakePHPでApache Solrを使おうと思ったので、datasourceとか誰か作ってないかなぁと思って探したんだけど、見つからなかったのでざっと書いてみた。
Solrとの通信部分は自分で作成するのが面倒だったので、「solr-php-client
」を利用させて頂いています。
う〜ん助かる♪
作成したファイルは、以下の通り。
※ファイル名は、solr_source.php
<?php App::import('Vendor', 'Service', array( 'file'=>'Solr'.DS.'Service.php' )); class SolrSource extends DataSource { var $fields = null; var $_baseConfig = array( 'driver'=>'solr' , 'host'=>'localhost' , 'port'=>'8983' , 'path'=>'/solr/' , 'schema'=>'example' ); function __construct($config = null, $autoConnect = true) { parent::__construct($config); if ($autoConnect) { return $this->connect(); } else { return true; } } function connect() { $config = &$this->config; $this->connection = new Apache_Solr_Service($config['host'], $config['port'], $config['path']); return true; } function close() { unset($this->connection); return true; } function isConnected() { return true; } function describe(&$model) { $cache = parent::describe($model); if ($cache != null) { return $cache; } $schema = $this->connection->schema(); if ( empty($schema)) { return null; } else { $schema = $this->connection->schema(); $fields = $schema->fields; } $fields = $this->fields; $this->__cacheDescription($this->fullTableName($model, false), $fields); return $fields; } function listSources() { return array( $this->config['schema'] ); } private function __service() { $config = &$this->config; return new Apache_Solr_Service($config['host'], $config['port'], $config['path']); } function read(&$model, $query = array( ), $recursive = null) { $results = array( ); $conditions = $this->_generateCondition($query); $options = array( ); if (isset($query['options'])) { $options = $query['options']; } $this->_queriesLog[] = array( 'query'=>$conditions , 'error'=>$this->error , 'affected'=>$this->affected , 'numRows'=>$this->numRows , 'took'=>$this->took ); $results = $this->connection->search($conditions, $query['limit'] * ($query['page'] - 1), $query['limit'], $options); if ($model->findQueryType === 'count') { $count = 0; if (isset($results->response->numFound)) { $count = $results->response->numFound; } return array( array( array( 'count'=>$count ) ) ); } else { $docs = $results->response->docs; unset($results); if (isset($options['response']) && $options['response'] === 'class') { // if options['response'] equals 'class', return value type is iterator class return $docs; } else { foreach ($docs as $doc) { $tmp[] = (array) ($doc->getIterator()); } return isset($tmp) ? $tmp : array( ); } } } private function & _generateCondition(&$query = null) { $conditions = "*:*"; if (isset($query['conditions'])) { $conditions = $query['conditions']; if (is_array($conditions)) { $queryString = ''; foreach ($conditions as $key=>$value) { if ($key === 'AND' || $key === 'OR') { if (is_array($value)) { foreach ($value as $k=>$v) { if (is_array($v)) { foreach ($v as $vv) { $this->_generateQuery(&$queryString, &$key, &$k, &$vv); } } else { $this->_generateQuery(&$queryString, &$key, &$k, &$v); } } } } else { if (is_array($value)) { foreach ($value as $v) { $this->_generateQuery(&$queryString, 'OR', &$key, &$v); } } else { $this->_generateQuery(&$queryString, 'OR', &$key, &$value); } } } if (get_magic_quotes_gpc() == 1) { $queryString = stripslashes($queryString); } $conditions = &$queryString; } } return $conditions; } private function & _generateQuery($query, $operator, $key, $value) { if(!isset($value)){ return $query; } $first = empty($query); if ($operator === 'AND') { if (!$first) { $query .= ' AND '; } } else { if (!$first) { $query .= ' OR '; } } $first = false; $query .= $key.':'.$value; return $query; } function create(&$model, $fields, $values) { return $this->_insertOrUpdate($model, $fields, $values); } function update(&$model, $fields, $values) { return $this->_insertOrUpdate($model, $fields, $values); } private function _insertOrUpdate(&$model, $fields, $values) { $data = array_combine($fields, $values); if ($data === false) { return false; } $doc = new Apache_Solr_Document(); foreach ($data as $field=>$value) { $doc-> { $field } = $value; } $updateResponse = $this->connection->addDocument($doc); unset($documents); if (array_key_exists($model->table, $this->_cache)) { unset($this->_cache[$model->table]); } return true; } function delete(&$model, $conditions = null) { $query = '<delete>'; foreach ($conditions as $id) { $query = '<id>'; $query = $id; $query = '</id>'; } $query .= '</delete>'; $this->connection->delete($query); unset($documents); if (array_key_exists($model->table, $this->_cache)) { unset($this->_cache[$model->table]); } return true; } function fullTableName(&$model, $quote = true) { if (is_object($model)) { $table = $model->table; } else if (isset($this->config['prefix'])) { $table = $this->config['prefix'].strval($model); } else { $table = strval($model); } if ($quote) { return $this->name($table); } return $table; } function calculate(&$model, $func, $params = array( )) { // とりあえずこれでいいかな return array( 'count'=>true ); } function begin(&$model) { return true; } function commit(&$model) { return $this->connection->commit(); } /** * Get the query log as an array. * * @param boolean $sorted Get the queries sorted by time taken, defaults to false. * @return array Array of queries run as an array * @access public */ function getLog($sorted = false, $clear = true) { if ($sorted) { $log = sortByKey($this->_queriesLog, 'took', 'desc', SORT_NUMERIC); } else { $log = $this->_queriesLog; } if ($clear) { $this->_queriesLog = array( ); } return array( 'log'=>$log , 'count'=>$this->_queriesCnt , 'time'=>$this->_queriesTime ); } } ?>
※CRUDのうち、create/update/deleteも一応書いたけど、一括登録の方法が思いつかなかったので、一切使ってません(テストもしていません)…
使い方
まず、solr-php-clientをダウンロードして、Vendorの下におきます。
次に、前述のsolr_source.phpを以下のディレクトリに設置
app/models/datasources/
これでソースの設置は完了。
今度は、db configを定義する。
app/config/database.phpに、以下の記述を追加
※接続情報は、自分の環境に合わせてください。
var $solr = array( 'datasource' => 'solr' ,'schema' => '(※solrのschema.xmlに記述しているスキーマ名)' ,'host' => 'localhost' ,'port' => '8983' ,'path' => '/solr/' );
次にsolrのschemaを利用するため、modelを作成する。
とりあえず、名前はschema名と同じにしておくとわかりやすいかも。
modelの作り方は、通常のRDBのモデルと同じ(アソシエーションは使えません)
ただし、該当のmodelには以下のように使用するdb configを指定します。
public $useDbConfig = 'solr';
あとは、controllerなどで使用する際に、他のRDBの検索と同じように使用するだけ。
※paginatorも使用できますよ。
conditionsで少しだけ特殊なのが、複数項目の条件文を書くときかな。
あと、エスケープ処理や重み付けもcontroller側でやってください。
以下、conditionsの作成サンプルです。
//$wordに検索ワード $word = 'ほげほげ'; //エスケープ処理 $word= Apache_Solr_Service::escape($word); // スペース区切りの場合の順位性を保つために、"(ダブルクォーテーションで囲む) $word= '"'.$word.'"'; // 重み付けも検索条件側でセット $query['title'] = $word."^2.0"; $query['content'] = $word; $query['comment'] = $word; //全てORでつなげる場合は、以下のように"OR"をkeyにした連想配列で渡してやる $conditions = array( 'OR'=>$query ); //ちなみに$conditionに配列をセットするとORで検索条件をつなぎます /* $conditions = &$query */ // paginatorを使う場合は、以下のような感じです。 $this->paginate = array( 'conditions'=>$conditions , 'limit'=>$limit ); $this->set('hogehoge', $this->paginate('モデル名'));
複雑な処理など、殆ど試していないので上手く動かないところがあ〜るか〜もね〜
PHPでの定数配列について
cakePHPの勉強しているが、そもそもPHPの自体の知識が不十分(だったらPHPの基礎から勉強しろよって話しだけどw)。
static な 定数配列みたいなものをPHPでやろうと思ったけど、標準では良い方法がない。
だけど、単なる配列の変数宣言だけだと、変なプログラマが配列の中身を変えてしまう可能性があるし…
なので、以下の方法で簡単実装してみたけど、こんなやり方は普通しないのかなぁ?
class Domains { // ユーザーの種類をテーブル上はcodeで保存 private static $userTypes = array( "admin" => array('code'=>'1', 'label'=>"管理者") ,"normal" => array('code'=>'2', 'label'=>"一般会員") ,"charged" => array('code'=>'3', 'label'=>"有料会員") ); public static function userTypes() { // 単なるconstのコピー配列なので、受け取った配列は変更可能だけど他のクラスへの影響はないからという割り切りです $tmp = self::$userTypes; return $tmp; } }
配列の変数を代入したら、コピーとして変数に代入されるということを知った今日この頃w
PHPerへの道のりは、遠いなww
Form->inputで出力されるラベルの名称をオプションではなく別ファイルで定義する
勉強がてらcakePHPのFormヘルパーのinputを使ってみたんだけど、ラベルの定義('label'=>'ほげほげ')をいちいちオプションで与えるのが面倒。
また複数人で開発している場合は、気を付けないと同じ項目でも別名になってしまう可能性があるかなぁと。
SAStrutsみたいにpropertiesファイルに(labels.フィールド名)みたいに一箇所で定数を切ることで、項目の日本語名を置換できるようにしたい。
ということで、Formヘルパーを変更してみた。
ちなみにCakePHPの経験はあまりない(PHP自体きちんとやっていない)ので、もっと一般的なやり方があるのかもしれないが、とりあえず勉強ということで。
※もし他に良い方法があるなら教えて欲しいです…
変更は、以下の手順。
- labels.phpを作成し、そのファイルに「labels.フィールド名」の形で定数を定義する。
- bootstrap.phpでlabels.phpを読み込むようにする。
- Formヘルパーで、(labels.フィールド名)の定義があればその文言でラベルの文言を置換するように修正する。
- 定数定義したフィールド名でviewを作成する。
みたいな流れです。
具体的な修正は、以降のサンプルを参照。
1. labels.phpを作成し、ファイル内で「labels.フィールド名」の形で定義をする。
app/configフォルダに、labels.phpファイルを作成する。
<?php define('labels.login_id', 'ログインID'); define('labels.password', 'パスワード');
3. Formヘルパーで、(labels.フィールド名)の定義があればその文言でラベルの文言を置換する。
cake/libs/view/helpers/form.phpをapp/views/helpersにform.phpをコピーする。
そんでlabelメソッドを以下みたいな感じで修正
function label($fieldName = null, $text = null, $options = array()) { if (empty($fieldName)) { $view = ClassRegistry::getObject('view'); $fieldName = implode('.', $view->entity()); } if ($text === null) { // labelから定義を読み込む if(defined('labels.'.$fieldName)){ $text = constant('labels.'.$fieldName); } if($text === null) { if (strpos($fieldName, '.') !== false) { $text = array_pop(explode('.', $fieldName)); } else { $text = $fieldName; } if (substr($text, -3) == '_id') { $text = substr($text, 0, strlen($text) - 3); } $text = __(Inflector::humanize(Inflector::underscore($text)), true); } } if (is_string($options)) { $options = array('class' => $options); } if (isset($options['for'])) { $labelFor = $options['for']; unset($options['for']); } else { $labelFor = $this->domId($fieldName); } return sprintf( $this->Html->tags['label'], $labelFor, $this->_parseAttributes($options), $text ); }
4. 定数定義したフィールド名でviewを作成する。
以下のような感じで、Formヘルパーを使用する。
<?php echo $this->Form->input('login_id', array('type'=>'text')); echo $this->Form->input('password'); ?>
すると出力結果が、labels.phpで定義した定数になる。
当然だけど定数定義をしていても、Formヘルパーで別名を指定すれば、その名前が出力される。
<?php echo $this->Form->input('login_id', array('type'=>'text', 'label'=>'ログインIDだよー')); echo $this->Form->input('password'); ?>
ざっと修正しただけなので、あんまテストしてないけど、なんとなく動くことは確認したよっと。
さくらのレンタルサーバー(PHP.5.3.6)にcakePHP1.3.11をインストールしたときのメモ
最近はJava案件ばかりをやっていたけど、今後しばらくの間はcakePHPをやることになりそうなので、勉強を兼ねて自分が借りているさくらのレンタルサーバーにcakePHPをインストールすることにした。
ということで、備忘録。
はじめに
cakePHPをインストールする際に、どうせならPHPの最新でやろうとおもって、さくらで選べる最新のバージョン(5.3.6)を選択した。
で、インストールをすると以下のエラーが発生した。
Warning (2): strtotime() [function.strtotime]: It is not safe to rely on the system's timezone settings. You are *required* to use the date.timezone setting or the date_default_timezone_set() function. In case you used any of those methods and you are still getting this warning, you most likely misspelled the timezone identifier. We selected 'Asia/Tokyo' for 'JST/9.0/no DST' instead [CORE/cake/libs/cache.php, line 597]
どうやらPHP5.3でcakePHP1.3.11をインストールするときは、php.iniにタイムゾーンを設定しないといけないらしい。
※参考
泡史朗の戯れの日々: PHP5.3 タイムゾーン エラー?
と、いうことでさくらでは以下から設定可能みたいなので、設定した。
https://secure.sakura.ad.jp/rscontrol/rs/phpini
以下を一番最後に追記する。
date.timezone = Asia/Tokyo
2. ドキュメントルートに公開ディレクトリ(koiteku)を作成する。
/home/(ユーザー)/www/koiteku
5. 解凍したファイルを配置(インストール)
とりあえず、webrootのみ公開ディレクトリに置くことにし、あとは適当なところ。
mkdir ~/koiteku cp -r cake ~/koiteku/ cp -r app ~/koiteku/ mv /home/(ユーザー)/koiteku/app/webroot/* /home/(ユーザー)/www/koiteku
6. 設定ファイルの編集
vi /home/(ユーザー)/www/koiteku/index.php
※以下、抜粋。
/** * The full path to the directory which holds "app", WITHOUT a trailing DS. * */ if (!defined('ROOT')) { /* define('ROOT', dirname(dirname(dirname(__FILE__)))); */ define('ROOT', DS.'home'.DS.'(ユーザー)'.DS.'koiteku'); } /** * The actual directory name for the "app". * */ if (!defined('APP_DIR')) { /* define('APP_DIR', basename(dirname(dirname(__FILE__)))); */ define ('APP_DIR', 'app'); } /** * The absolute path to the "cake" directory, WITHOUT a trailing DS. * */ if (!defined('CAKE_CORE_INCLUDE_PATH')) { /* define('CAKE_CORE_INCLUDE_PATH', ROOT); */ define ('CAKE_CORE_INCLUDE_PATH', DS.'home'.DS.'(ユーザー)'.DS.'koiteku'); }
7. Security.saltとSecurity.cipherSeedを変更
Security.saltとSecurity.cipherSeedを変更しないと怒られるので、修正。
vi /home/(ユーザー)/koiteku/app/config/core.php
※以下、抜粋(もちろん実際の設定情報じゃないですよ)
/** * A random string used in security hashing methods. */ /* Configure::write('Security.salt', 'DYhG93b0qyJfIxfs2guVoUubWwvniR2G0FgaC9mi'); */ Configure::write('Security.salt', 'hogehoge'); /** * A random numeric string (digits only) used to encrypt/decrypt strings. */ /* Configure::write('Security.cipherSeed', '76859309657453542496749683645'); */ Configure::write('Security.cipherSeed', '111111111');
8. DB情報を設定
DB情報は、以下のdatabase.php.defaultをdatabase.phpでコピーし、database.phpに対し、さくらから教えてもらったDB情報を設定
cp /home/(ユーザー)/koiteku/app/config/database.php.default /home/(ユーザー)/koiteku/app/config/database.php vi database.php
9. .htaccessを設定する。
※前述までの内容で、cakeのトップページは表示されるが、実際にコントローラーなどを作成すると正しくリダイレクトされない…
ということで、.htaccessにRewriteBaseを設定する。
ちなみに設定していないと以下のエラーメッセージがでる
mod_rewrite: maximum number of internal redirects reached. Assuming configuration error. Use 'RewriteOptions MaxRedirects' to increase the limit if neccessary
/home/(ユーザー)/koiteku/.htaccess
RewriteEngine on RewriteBase / RewriteRule ^$ app/webroot/ [L] RewriteRule (.*) app/webroot/$1 [L]
/home/(ユーザー)/koiteku/app/.htaccess
RewriteEngine on RewriteBase /app RewriteRule ^$ webroot/ [L] RewriteRule (.*) webroot/$1 [L]
10. 作成したサブドメインにアクセスして、何もエラーがなければOK
Javaで写真をトイカメラ風にしたい
いつものように、ふとJavaで写真をトイカメラ風にしてみたいと思ったので、早速試してみました。
簡単に実現したかたので、Jerry Huxtableさんという人が公開しているJava Image Filtersを使ってみました。
http://www.jhlabs.com/ip/filters/index.html
ライセンスは、 Apache License, Version 2.0とのこと。
ダウンロードすると、ソースとjarが提供されているですが、なぜかjarが少し古いようなのでソースからantでビルドしてFilter.jarをつくり直してから、それをビルドバスに通しました。
作り方は、Photoshopでトーンカーブを使ったトイカメラ風の写真にする方法が紹介されていたので、それをそのまま同じように適応しただけです。
http://photoshopvip.net/archives/13661
実際に動かして作った画像は、以下。
↓トイカメラ風に加工した後の写真
いかがですか?個人的には、これだけでもいい感じかなぁと思っていますw
コードは、以下の通り。
package sample.imagefilter; import java.awt.image.BufferedImage; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import javax.imageio.ImageIO; import com.jhlabs.image.Curve; import com.jhlabs.image.CurvesFilter; import com.sun.image.codec.jpeg.JPEGCodec; import com.sun.image.codec.jpeg.JPEGEncodeParam; import com.sun.image.codec.jpeg.JPEGImageEncoder; /** * トイカメラ風の画像を作成するサンプルです。 * トーンカーブのみで表現しています。 */ public class ToycameraSample { public static void main(String[] args) { try { BufferedImage src = ImageIO.read(new File("sample.jpg")); // 赤 Curve red = new Curve(); red.x = new float[] { 0, 90 / 255f, 170 / 255f, 217 / 255f, 1 }; red.y = new float[] { 0, 40 / 255f, 190 / 255f, 255 / 255f, 1 }; // 緑 Curve green = new Curve(); green.x = new float[] { 0, 62 / 255f, 190 / 255f, 1 }; green.y = new float[] { 0, 61 / 255f, 220 / 255f, 1 }; // 青 Curve blue = new Curve(); blue.x = new float[] { 0, 255 / 255f }; blue.y = new float[] { 30 / 255f, 225 / 255f }; Curve[] rgb = new Curve[] { red, green, blue }; CurvesFilter cf = new CurvesFilter(); cf.setCurves(rgb); BufferedImage dist = cf.filter(src, null); // ImageIO.writeだと画質が悪いみたいです。 // ImageIO.write(dist, "jpg", new File("sample_after.jpg")); OutputStream os = null; try { os = new FileOutputStream(new File("sample_after.jpg")); JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder(os); JPEGEncodeParam params = encoder.getDefaultJPEGEncodeParam(dist); params.setQuality(0.95f, false); encoder.encode(dist, params); } finally { os.close(); } } catch (IOException e) { e.printStackTrace(); } } }
画像編集の知識がないので少し調べたんだけど、画像の加工って奥が深いなぁと思いましたw
画像の知識は0だけど、類似画像の検索をしたかったのでLireを使ってみた
ここで言ってる類似画像検索とは、以下のサイトなどでもある特定の画像と似た画像を検索するというやつですね。
画像についての知識は0なので、自分で1から作るのは時間がかかり過ぎるので、ライブラリを探すことにした。
で、少し調べたら Lireというのがヒットした。
Lireがどういう仕組みで動くとかは、こちらに書いてあるみたいだけど、私には理解不能ですw
興味のある方PDFなどの参考資料もあるようなので読み込んでみては?
http://www.semanticmetadata.net/wiki/doku.php?id=lire:lire#how_does_lire_actually_work
ライセンスは、「Gnu GPL license」のようです。
まず、ダウンロードは、以下のサイトからLire-0.8.zipを落としました。
http://sourceforge.net/projects/caliph-emir/files/
※この時点で2010/3/11が最終の更新みたい。
落としたzipファイルは解凍しておきます。
で、lire.jarをビルドパスに追加します。
また、関連jarとして圧縮ファイルのlibフォルダに同梱されていた以下のjarもビルトパスに追加。
- lucene-core-3.0.1.jar
- Jama-1.0.2.jar
- caliph-emir-cbir.jar
以上で準備は完了。
まず、類似画像を検索するために、検索対象となる画像のIndex(索引)を作ります。
コードは、wikiにサンプルが載っているのでほぼそのまま使います。
package sample; import java.io.File; import java.io.FileInputStream; import net.semanticmetadata.lire.DocumentBuilder; import net.semanticmetadata.lire.DocumentBuilderFactory; import org.apache.lucene.analysis.SimpleAnalyzer; import org.apache.lucene.document.Document; import org.apache.lucene.index.IndexWriter; import org.apache.lucene.store.FSDirectory; public class LireCreateIndex { private final static String TARGET_DIR = "C:\\Users\\Public\\Pictures\\Sample Pictures"; private final static String INDEX_PATH = "E:\\lire\\index"; public static void main(String[] args) { DocumentBuilder builder = DocumentBuilderFactory.getExtensiveDocumentBuilder(); try { // Indexを場所のフォルダを指定し、IndexWriteをNEWする!! IndexWriter iw = null; iw = new IndexWriter(FSDirectory.open(new File(INDEX_PATH)), new SimpleAnalyzer(), true, IndexWriter.MaxFieldLength.UNLIMITED); // 検索対象とするファイルを置いてあるフォルダを指定し、その配下のファイルを対象にIndexを作る File dir = new File(TARGET_DIR); File[] files = dir.listFiles(); for (File file : files) { Document doc = null; try { doc = builder.createDocument(new FileInputStream(file), file.getName()); iw.addDocument(doc); } catch (Exception e) { // 画像読み込みエラーなど e.printStackTrace(); } } iw.optimize(); iw.close(); } catch (Exception e) { // Indexファイル作成エラーなど e.printStackTrace(); System.exit(0); } } }
今回は面倒なので、Windowsのサンプルピクチャのフォルダ配下の画像を類似画像検索の対象とするようにしました。
Indexを作ったところで、いよいよ類似画像検索です。
package sample; import java.awt.image.BufferedImage; import java.io.File; import java.io.FileInputStream; import javax.imageio.ImageIO; import net.semanticmetadata.lire.DocumentBuilder; import net.semanticmetadata.lire.ImageSearchHits; import net.semanticmetadata.lire.ImageSearcher; import net.semanticmetadata.lire.ImageSearcherFactory; import org.apache.lucene.index.IndexReader; import org.apache.lucene.store.FSDirectory; public class LireSimilarSearch { private final static String INDEX_PATH = "E:\\lire\\index"; public static void main(String[] args) { try { // 検索対象のIndexを読むReaderを作る IndexReader reader = IndexReader.open(FSDirectory.open(new File(INDEX_PATH))); // 画像を検索するためのサーチャーを作成 // カラーだけのサーチャーとか色々あるみたいです。 // あと、自分でどこに重みを置いて検索するかなども指定できるようです。 // http://www.semanticmetadata.net/wiki/doku.php?id=lire:searchindex ImageSearcher searcher = ImageSearcherFactory.createDefaultSearcher(); // ターゲットの画像を指定 FileInputStream imageStream = new FileInputStream( "C:\\Users\\Public\\Pictures\\Sample Pictures\\sample.jpg"); BufferedImage bimg = ImageIO.read(imageStream); ImageSearchHits hits = null; // 画像検索 hits = searcher.search(bimg, reader); // 似ている画像トップファイルを5件をコンソールに出力 for (int i = 0; i < 5; i++) { System.out .println(hits.score(i) + ": " + hits.doc(i).getField(DocumentBuilder.FIELD_NAME_IDENTIFIER) .stringValue()); } } catch (Exception e) { // ファイル読み込みエラーなど e.printStackTrace(); } } }
以下、が出力結果ですね
スコア | :ファイル名 |
0.5416657 | : Koala.jpg |
0.33018762 | : Desert.jpg |
0.29604518 | : Lighthouse.jpg |
0.2810576 | : Tulips.jpg |
0.2410211 | : Hydrangeas.jpg |
超簡単だね♪
画像の知識がない自分が1から作るのは大変だけど、とりあえず試すならLireで十分じゃないかなぁ。
色彩とか何に類似の重みを置くかなど自由に指定できるようなので、色々試したいな。