poniedziałek, 24 grudnia 2012

Bitmapowy zawrót głowy

Dziś bez zbędnych wprowadzeń i opisów - po prostu fragmenty kodu odpowiedzialne za konwersję pomiędzy trzema klasami odpowiedzialnymi za grafikę w .NET: Bitmap, BitmapImage oraz BitmapSource. Natomiast w ramach bonusu świątecznego konwersja z i do Byte[], która z pewnością w przypadku trzymania obrazów w plikach/bazach danych przyda się "od kopa". Przedstawiony kod jest w postaci metod rozszerzających (ang. extension methods), więc umożliwi przejrzyste i proste wykorzystanie.

Bitmap

using System;
using System.Drawing;
using System.Windows.Media.Imaging;
using System.IO;
using System.Drawing.Imaging;
using System.Windows.Interop;

namespace Runaurufu.Extension
{
    public static class ExtensionBitmap
    {
        public static BitmapImage ToBitmapImage(this Bitmap A)
        {
            using (MemoryStream ms = new MemoryStream())
            {
                A.Save(ms, ImageFormat.Bmp);
                ms.Position = 0;
                BitmapImage bi = new BitmapImage();
                bi.BeginInit();
                bi.StreamSource = ms;
                bi.CacheOption = BitmapCacheOption.OnLoad;
                bi.EndInit();
                return bi;
            }
        }

        [System.Runtime.InteropServices.DllImport("gdi32.dll")]
        private static extern bool DeleteObject(IntPtr hObject);

        public static BitmapSource ToBitmapSource(this Bitmap A)
        {
            IntPtr hBitmap = A.GetHbitmap();
            BitmapSource result;
            try
            {
                result = Imaging.CreateBitmapSourceFromHBitmap(hBitmap, IntPtr.Zero, System.Windows.Int32Rect.Empty, System.Windows.Media.Imaging.BitmapSizeOptions.FromEmptyOptions());
            }
            finally
            {
                DeleteObject(hBitmap);
            }

            return result;
        }

        public static Byte[] ToByteArray(this Bitmap A)
        {
            using (MemoryStream ms = new MemoryStream())
            {
                A.Save(ms, ImageFormat.Bmp);
                ms.Position = 0;
                return ms.ToArray();
            }
        }
    }
}

BitmapSource

using System;
using System.Drawing;
using System.IO;
using System.Windows.Media.Imaging;

namespace Runaurufu.Extension
{
    public static class ExtensionBitmapSource
    {
        public static BitmapImage ToBitmapImage(this BitmapSource A)
        {
            return A.ToBitmapImage(new BmpBitmapEncoder());
        }

        public static BitmapImage ToBitmapImage(this BitmapSource A, BitmapEncoder encoder)
        {
            BitmapImage bi = new BitmapImage();

            using (MemoryStream ms = new MemoryStream())
            {
                encoder.Frames.Add(BitmapFrame.Create(A));
                encoder.Save(ms);
                ms.Position = 0;

                bi.BeginInit();
                bi.StreamSource = ms;
                bi.CacheOption = BitmapCacheOption.OnLoad;
                bi.EndInit();
            }

            return bi;
        }

        public static Bitmap ToBitmap(this BitmapSource A)
        {
            return A.ToBitmap(new BmpBitmapEncoder());
        }

        public static Bitmap ToBitmap(this BitmapSource A, BitmapEncoder encoder)
        {
            System.Drawing.Bitmap bitmap;
            using (MemoryStream outStream = new MemoryStream())
            {
                encoder.Frames.Add(BitmapFrame.Create(A));
                encoder.Save(outStream);
                bitmap = new System.Drawing.Bitmap(outStream);
            }
            return bitmap;
        }

        public static Byte[] ToByteArray(this BitmapSource A)
        {
            return A.ToByteArray(new BmpBitmapEncoder());
        }

        public static Byte[] ToByteArray(this BitmapSource A, BitmapEncoder encoder)
        {
            using (MemoryStream memoryStream = new MemoryStream())
            {
                encoder.Frames.Add(BitmapFrame.Create(A));
                encoder.Save(memoryStream);
                return memoryStream.GetBuffer();
            }
        }
    }
}

BitmapImage

using System;
using System.Drawing;
using System.IO;
using System.Windows.Media.Imaging;

namespace Runaurufu.Extension
{
    public static class ExtensionBitmapImage
    {
        public static Bitmap ToBitmap(this BitmapImage A)
        {
            return A.ToBitmap(new BmpBitmapEncoder());
        }

        public static Bitmap ToBitmap(this BitmapImage A, BitmapEncoder encoder)
        {
            Bitmap bitmap;
            using (MemoryStream outStream = new MemoryStream())
            {
                encoder.Frames.Add(BitmapFrame.Create(A));
                encoder.Save(outStream);
                bitmap = new System.Drawing.Bitmap(outStream);
            }
            return bitmap;
        }

        public static Byte[] ToByteArray(this BitmapImage A)
        {
            return A.ToByteArray(new BmpBitmapEncoder());
        }

        public static Byte[] ToByteArray(this BitmapImage A, BitmapEncoder encoder)
        {
            Byte[] byteArr;
            using (MemoryStream ms = new MemoryStream())
            {
                encoder.Frames.Add(BitmapFrame.Create(A));
                encoder.Save(ms);
                ms.Position = 0;
                byteArr = ms.GetBuffer();
            }
            return byteArr;
        }
    }
}

Byte[]

using System;
using System.Drawing;
using System.IO;
using System.Windows.Media.Imaging;

namespace Runaurufu.Extension
{
    public static class ExtensionByte
    {
        public static Bitmap ToBitmap(this Byte[] A)
        {
            using (MemoryStream ms = new MemoryStream(A))
            {
                return new Bitmap(ms);
            }
        }

        public static BitmapImage ToBitmapImage(this Byte[] A)
        {
            using (MemoryStream ms = new MemoryStream(A))
            {
                ms.Position = 0;
                BitmapImage bi = new BitmapImage();
                bi.BeginInit();
                bi.StreamSource = ms;
                bi.CacheOption = BitmapCacheOption.OnLoad;
                bi.EndInit();
                return bi;
            }
        }
    }
}

Warto zwrócić uwagę, że w przypadku BitmapSource czyli obrazach rodem z WPF/Silverlighta konieczne jest podanie odpowiedniego enkodera - wybór np. JpegEncoder na obrazie z kanałem alfa (przeźroczystość) sprawi, że ów kanał alfa trafi szlag... :) Dodatkowo z oczywistych przyczyn nie ma metody do konwersji BitmapImage -> BitmapSource - a jeśli ktoś szuka bo z jakiś powodów potrzebuje to może wykorzystać ten fragment:
        public static BitmapSource ToBitmapSource (this BitmapImage A)
        {
            return (BitmapSource)A;
        }

A wszystkim, którzy choć od czasu do czasu zaglądają w me skromne progi życzę Wesołych Świąt :)

Keywords: C#, . NET, Bitmap, BitmapImage, BitmapSource, Bitmap conversions, Silverlight, WPF, WinForm, Image, Picture, Transparent BitmapImage.

niedziela, 4 listopada 2012

Ree7 Tile Toolkit i obrazy na kafelkach

Jeśli kiedykolwiek poczujecie chęć stworzenia własnych "kafelek" do swojej aplikacji to zapewne odnajdziecie w czeluściach Internetu bibliotekę "Ree7 Tile Toolkit for Windows Phone 7" umożliwiającą tworzenie całkowicie dowolnych kafelek. I żeby była jasność - poprzez "całkowitą dowolność" mam na myśli tworzenie zawartości Tile w ten sam sposób co zwykłego User Control (oczywiście Tile musi być statyczny, więc animacji w nim nie odtworzymy) - czyli tak za pomocą XAML jak i bezpośrednio przy użyciu kodu w VB czy C#.

Niestety przy tworzeniu własnych kafelek napotkać możemy jeden podstawowy problem wynikający ze sposobu obsługi obrazów w Silverlight. Obrazy generalnie nie są ładowane do pamięci aż do momentu, w którym są wyświetlane, tak więc tworzone kafelki po prostu renderowane są bez elementów pochodzących z plików graficznych. Dodatkowo w przykładowej aplikacji biblioteki ten problem jest nie dostrzegalny, gdyż tam kafelki są uprzednio wyświetlane w drzewie aplikacji:
SimpleTileContainer.Children.Add(myCustomTile1);
a tworzone są dopiero po kliknięciu w odpowiedni przycisk.
ShellTile.Create(new Uri("/MainPage.xaml?src=SampleTile", UriKind.Relative), myCustomTile1.GetShellTileData());

wtorek, 16 października 2012

Syndykacja czyli jak wyświetlić treść na swojej stronie?

Kanały treści, feedy lub po prostu RSS/Atom*. Istnieje wiele określeń na nośnik informacji używany w syndykacji treści stron internetowych, które to nieraz są stosowane wymiennie. W zasadzie dziś mało kto zwraca uwagę na to co kryje się pod magicznym guzikiem symbolizującym właśnie kanał treści danej witryny. Ale czym jest owa syndykacja, której próżno szukać w słownikach języka polskiego (link | link)? W uproszczeniu jest to publikowanie treści podmiotu przez inny podmiot, który tej treści nie utworzył. Definicja może i krótka, ale pewnie nie do końca zrozumiała, dlatego posłużę się przykładem nie związanym z internetem:
Stacja telewizyjna A nakręciła materiał z koncertu super gwiazdy i udostępnia go na swoim kanale. Jednocześnie stacja ta pozwala innym stacjom na używanie tego materiału na swoich kanałach (zwykle za opłatą licencyjną). I właśnie takie udostępnianie innym nazywamy syndykacją.
W świecie internetu gro procesu syndykacji zostało uproszczone do generowania przez właściciela treści pliku z jej zawartością, który to umieszcza na swoim serwerze. Rzadko też spotykamy się z odpłatnymi kanałami, a jeśli już to są one konsekwencją płatnego dostępu do całego serwisu. Dość po słowie, że świat idzie na przód i syndykacja w obecnym obliczu jest jednym z elementów, które powodują odejście starej wizji mediów w niebyt.

JavaScript i AJAX

 Ale wracając do tematu wpisu, czyli wyświetlania kanału treści na swojej witrynie. Niestety najprostsze i najkorzystniejsze dla właściciela hostingu (transfer, czas procesora) podejście wykorzystujące JavaScript w wielu przypadkach nie sprawdzi się z racji polityki pochodzenia z tej samej domeny. Oczywiście jeśli chcemy agregować treści znajdujące się w serwisach tej samej domeny ten problem nas nie dotyczy i bez problemu możemy wykorzystać AJAX (Tak kanały w standardzie RSS jak i w Atom korzystają z XML) choćby z poniższym kodem:
function GetXHR()
{
 var xmlHttp = null;
 try
 {   // Firefox, Opera 8.0+, Safari
  xmlHttp=new XMLHttpRequest();
 }
 catch (e)
 {   // Internet Explorer
  try
  {
   xmlHttp=new ActiveXObject("Msxml2.XMLHTTP");
  }
  catch (e)
  {
   xmlHttp=new ActiveXObject("Microsoft.XMLHTTP");
  }
 }
 if(xmlHttp == null)
  alert("Browser does not support HTTP Request");
 return xmlHttp;
}

window.addEventListener('load', function()
{
 var chk=setInterval(function()
 {  
  doc = document.getElementsByTagName("body");
  if(doc.length == 0 || doc[0].innerHTML.length < 500)
  { return; }
  clearInterval(chk);
  
  var xhr = GetXHR();
  xhr.onreadystatechange=function()
  {
   if(this.readyState == 4 && this.status == 200)
   {
    document.getElementById("articleStuffShowRSSHere").innerHTML = "<a href='"+
     this.responseXML.getElementsByTagName("entry")[0].getElementsByTagName("link")[4].getAttribute("href")+"'>"+
     this.responseXML.getElementsByTagName("entry")[0].getElementsByTagName("title")[0].textContent+
     "</a>";
   }
   
  };
  xhr.open("GET" , '/feeds/posts/default', true);
  xhr.send();  
 },100);
}, false);
Przykład ten wyświetla obok link do najnowszego artykułu z tego bloga:

PHP i cache po stronie serwera

Jednak jeśli nasz serwis znajduje się w innej domenie to z usług JavaScript skorzystać nie będzie nam dane. Oczywiście możemy próbować korzystać z różnego rodzaju sztuczek lub zewnętrznych serwisów pobierających pliki syndykacji za nas... ale raczej nie o półśrodki tu chodzi, a o mniej lub bardziej profesjonalne i autonomiczne rozwiązanie. PHP na szczęście umożliwia nam pobranie plików z zewnętrznych serwisów i swobodną manipulację na nich, tak więc problem wspólnej domeny odpada. Pojawia się za to inny problem, a mianowicie problem z wydajnością - w przypadku JavaScript cały proces de facto zrzucaliśmy na użytkownika i z punktu widzenia serwera nie odczulibyśmy różnicy przy syndykacji z 2 kanałów lub z 200 kanałów (Choć użytkownik by zapewne odczuł). Przy korzystaniu z PHP natomiast jeśli każda wizyta użytkownika miałaby uruchamiać mechanizm pobierania plików, ich parsowania i transformacji do finalnej postaci prezentowanej użytkownikowi to nie potrzeba agregacji z 200 kanałów, ale wystarczy 1 kanał i 200 użytkowników naszego serwisu byśmy odczuli ten sam efekt. Co więcej istnieje realna groźba, że właściciel serwisu, z którego dane pobieramy uzna nas za źródło ataku DoS (ang. Denial-of-service), - bo wszak wszystkie żądania pobrania będą pochodzić z naszego serwera - i zablokuje nam dostęp do kanału.
Stąd też konieczny jest mechanizm zapisu pobranych danych w taki sposób aby odwiedziny nowych użytkowników nie generowały transferu danych pomiędzy serwisami, a korzystały z już ściągniętych danych. Tu należy się zastanowić na którym etapie procesu przygotowywania do prezentacji należy dane zapisać, bo w zależności od tego jak chcemy je wykorzystać inny moment tego procesu może okazać się najefektywniejszy. W poniższym przykładzie zapis do pliku na serwerze odbywa się po przygotowaniu całości prezentowanego komponentu, a podobny mechanizm można z powodzeniem wykorzystać do zapisu całych stron, których wygenerowanie wymaga wielu obliczeń lub zapytań do baz danych, a których treści nie ulegają częstej zmianie.
Generalnie kod składa się z dwóch części, pierwsza - dłuższa to funkcja przeprowadzająca pobranie, obróbkę i zapis treści na dysk serwera:
<?php
function prepare($ident)
{
 // download data and parse them with SimleXmlElement
 $content = file_get_contents('http://runaurufu.blogspot.com/feeds/posts/default');
 $x = new SimpleXmlElement($content);

 // get data which we want to use
 $entryName = $x->entry[0]->title."";
 $entryUrl = "";
 foreach ($x->entry[0]->link as $value)
 {
  $aux = $value->attributes();
  if($aux['rel']."" == 'alternate')
  {
   $entryUrl = $aux['href']."";
  }
 }

 $blogTitle = $x->title."";
 $blogSubTitle =  $x->subtitle."";
 $blogUrl = "";
 foreach ($x->link as $value)
 {
  $aux = $value->attributes();
  if($aux['rel']."" == 'alternate')
  {
  $blogUrl = $aux['href']."";
  }
 }

 // prepare for cacheing 

 //start output buffering
 ob_start();

 // here do entire presentation generation
 ?>
 <a href="<?php echo $entryUrl; ?>"><?php echo $entryName; ?></a> @ <a href="<?php echo $blogUrl; ?>" title="<?php echo $blogSubTitle; ?>"><?php echo $blogTitle; ?></a>

 <?php
 // save output buffer to $data
 $data = ob_get_contents();
 // clear output buffer and stop buffering
 // (ob_end_flush(); can be used to stop buffering and display buffer content)
 ob_end_clean();

 $handle = fopen($ident, 'wb');
 if(flock($handle, LOCK_EX))
 {
  $bytes = fwrite($handle, $data);
  flock($handle, LOCK_UN);
 }
 fclose($handle);
 return $data;
}
?>

Druga część jest odpowiedzialna za sam proces wyboru, czy chcemy użyć dane zapisane na dysku, lub czy mimo wszystko pobieramy je od nowa:
<?php
$cacheTime = 86400;
$ident = "myFeed.cache";
if(!is_file($ident))
{
 echo prepare($ident);
}
else
{
 if(filemtime($ident) + $cacheTime > time())
 {
  // show content of file
  echo file_get_contents($ident);
 }
 else
 {
  echo prepare($ident);
 }
}
?>
Oczywiście kod ten można w znacznej mierze zmodyfikować i sprawić, aby cały proces obsługi naszego cache wyglądał tak np:
<?php$cache = new Chors_Cache($identifier, $time);
if($cache->check())
{
 return $cache->fetchRender();
}
else
{
 $cache->startOutputCaching();
 // generate cached content begin
 ...
 // generate cached content end
 return $cache->endOutputCachingClean();
}?> 

Słowo na koniec

Jak widać obsługa kanałów syndykacji nie jest czymś trudnym a jedynym problemem na jaki możemy natrafić to konieczność stworzenia parsera wyłuskującego te dane, które nas interesują. Warto jeszcze wspomnieć, że o ile używanie AJAXa nie obciąża naszego serwera, to generuje zbyteczny ruch na serwerze udostępniającym kanał treści i z tego względu będąc "miłym sąsiadem" należałoby mimo wszystko zaprzęgnąć swój serwer do obsługi tych danych.

* Słowem wyjaśnienia: RSS i Atom o ile bywają synonimem udostępniania zawartości witryny to w rzeczywistości są tego udostępniania standardami, a nie samą nazwą procesu.

środa, 10 października 2012

Dźwięki w aplikacji na Windows Phone?

Jeśli chcemy mieć dźwięk przy zwykłej aplikacji, która nie jest pisana w XNA (czyli wg marketu nie jest grą) możemy go zdobyć na kilka sposobów... ale i tak koniec końców najlepiej użyć frameworku XNA :D

No, może nie wprost :] Ale o tym za chwilę.
Generalnie zapewne pierwszym sposobem, z którego większość by skorzystała jest natywna kontrolka "MediaElement", która fakt faktem udostępnia możliwość odtwarzania filmów i dźwięków niejako z marszu. Ot wrzucamy do XAML, dodajemy źródło naszej "zawartości medialnej" i mamy słyszalną zawartość. Czyli w skrócie wystarczy zrobić tak:
<MediaElement Height="100" Width="100" Name="media" Source="sound.mp3" AutoPlay="True" />
Prawda, że pięknie i prosto? No właśnie, nie do końca. Bo o ile jest to bardzo łatwe, to nadaje się de facto jedynie do odtwarzania zawartości na aktualnej stronie - wszak kontrolka jest sztywno przypisana do Page i po zmianie aktywnej strony po prostu przestanie wytwarzać jakiekolwiek dźwięki (co można obchodzić zmiennymi globalnymi, ale takie podejście jest po prostu marnowaniem zasobów telefonu). Z drugiej strony w przypadku wrzucenia apki używającej MediaElement do samo wyzwalającego się materiału (np. po pacnięciu w guzik) na market możemy spotkać się z odrzuceniem podczas certyfikacji, które to wynika z guidelines i domyślnego działania tej kontrolki:
  • When the user is already playing music on the phone when the application is launched, the application must not pause, resume, or stop the active music in the phone
    If the application plays its own background music or adjusts background music volume, it must ask the user for consent to stop playing/adjust the background music (e.g. message dialog or settings menu). This prompt must occur each time the application launches, unless there is an opt-in setting provided to the user and the user has used this setting to opt-in.
Czyli generalnie MediaElement jest super jeśli chcemy odtwarzać muzykę tła na pojedynczej stronie (lub wyświetlić filmik). Oczywiście nie możemy wtedy zapomnieć, że jeśli użytkownik słucha jakiejś muzyki to musimy się go zapytać czy wyraża zgodę na puszczenie muzyki z naszej aplikacji oraz zapewnić konfigurację dla naszego muzycznego tła:
  • If an application plays background music, the application must provide the user with configurable settings for both the background music, and the background music volume.
 Pewnym rozsądnym rozwiązaniem jest użycie BackgroundAudioPlayer, który pozwala nie tylko na puszczanie muzyki w tle wewnątrz całej aplikacji, ale również dzięki agentom wewnątrz innych aplikacji naszego telefonu (czyli możemy stworzyć odtwarzacz muzyki). Klasa ta jest bardzo przydatna, bo nie dość, że pozwala nam na wspomniane odtwarzanie tła, to również umożliwia przewijanie piosenek i wybór miejsca, od którego chcemy utwór odtworzyć (pod tym względem jest to nawet lepsze rozwiązanie od MediaPlayera z XNA). Rozwiązanie to ma jednak tą małą wadę, że plik z dźwiękiem albo musi być dostarczony z IsolatedStorage albo z zewnętrznego adresu URI (poradnik od MS dotyczący BackgroundAudioPlayer).

Jednak jeśli chcemy mieć dźwięki efektów, czyli np. odgłos gdaczącej kury po kliknięciu na guzik, to już tak MediaElement jak i BackgroundAudioPlayer się nie nadaje (bo m.in. wstrzymuje obecnie odgrywany dźwięk). Natomiast do tego celu nadaje się coś innego - klasa SoundEffect pochodząca z XNA :D
            using (var stream = TitleContainer.OpenStream("sound.wav"))
            {
                var effect = SoundEffect.FromStream(stream);
                FrameworkDispatcher.Update();
                effect.Play();
            }
Aż chciałoby się wykorzystać SoundEffect do odtwarzania muzyki w tle (zwłaszcza, że po stworzeniu instancji efektu możemy bez trudu odtwarzanie muzyki zapętlić)... niestety guideline jest bardzo stanowczy pod tym względem:
  • The SoundEffect class must not be used to play a continuous background music track in an application.
Ach Ci źli ludzie z Microsoftu! Na szczęście XNA daje nam dużo, dużo więcej możliwości, z których chyba najprzyjemniejszą jest połączenie klas Song i MediaPlayer:
            Song song = Song.FromUri("Nasze audio", new Uri("sound.mp3", UriKind.Relative));
            if (MediaPlayer.GameHasControl)
            {
                MediaPlayer.Play(song);
            }
Klasa ta jak widać nie tylko z gracją rozwiązuje problem nakładania się naszego muzycznego tła na muzykę odtwarzaną przez użytkownika dzięki polu GameHasControl i pozwala na odtwarzanie pojedynczych piosenek (Song), ale również umożliwia tworzenie ich całych kolekcji (SongCollection) oraz łatwe nawigowanie pomiędzy nimi. Tak więc myśląc o dźwięku w aplikacjach na Windows Phone bazujących na silverlight i tak prędzej czy później wykorzystamy biblioteki XNA, bo po prostu dają one ogromną swobodę i wbrew pozorom są bardzo łatwe w wykorzystaniu.

wtorek, 25 września 2012

PCA, Eigenfaces i rozpoznawanie twarzy

Czym jest PCA?

PCA czyli Principal Component Analysis czyli analiza głównych składowych jest jedną z metod statystycznych służących do analizy zbioru danych. Jeśli kogoś interesuje dokładniejsze omówienie tematu - polecam poszperać choćby po sieci (Wikipedia dla leniwych). Na potrzeby zaś tego wpisu i naszych rozważań uświadomimy sobie jedynie, że PCA umożliwia określenie w jakim stopniu dany klasyfikator jest zgodny z badanym obiektem. Innymi słowy -  jak daleko wartości elementu wzorowego do wartości badanego obiektu.

Czym jest rozpoznawanie twarzy?

Niby oczywistość, ale lepiej ją uściślić teraz, niż potem gubić się w zeznaniach. Z grubsza chodzi o to aby mając zbiór zdjęć z zidentyfikowanymi osobami móc określić czy nowe zdjęcie należy do jakiejś osoby z naszego zbioru (a jeśli tak, to której), czy też jest ono fotografią kogoś zupełnie nowego.

Etapy działania
  1. Przygotować zbiór uczący składający się z obrazów o tych samych wymiarach/rozdzielczości (szerokość i wysokość), w których oczy i usta osób znajdują się na podobnej wysokości.
  2.  Poddać obrazy operacji wyrównywania histogramów (histogram equalization) w celu redukcji wpływu różnicy oświetlenia.
  3. Obliczyć wartości pikseli dla średniego obrazu i odjąć ten obraz od każdego obrazu ze zbioru uczącego.
  4. Utworzyć macierz zawierającą wszystkie obrazy (każdy obraz to jeden wektor).
  5. Obliczyć wektory własne powyższej macierzy.
  6. Obliczyć PCA na podstawie wektorów własnych (jeśli decydujemy się na pominięcie niektórych wektorów to należy odrzucić te z najmniejszymi wartościami własnymi).
  7. Wykonać projekcję z wykorzystaniem PCA dla każdego obrazu ze zbioru uczącego.
  8. Projekcje nowych zdjęć porównywać z projekcjami obiektów już rozpoznanych i wyszukiwać wyników najbliższych.

Accord.NET

Czym jest Accord.NET? Z grubsza jest to framework oferujący bogactwo funkcji matematycznych i statystycznych, w tym m.in. rozkład na wektory własne, analizę PCA, LDA oraz wykrywanie twarzy (które działa trochę topornie, ale działa). Dlaczego wybrałem ten framework do realizacji tego zadania? Cóż popularną alternatywą byłoby użycie biblioteki OpenCV, która dość szeroko jest do tego celu (rozpoznawania twarzy) rekomendowana. Niestety cud ten technologii nie doczekał się sensownego wsparcia pod C# (wrappery działają różnie i generalnie w mojej opinii nie nadają się do użycia w produktach komercyjnych ze względu na moc różnych drobnych i nie tylko problemów z ich działaniem). Natomiast z racji tego, że Accord.NET jest bezproblemowy w użyciu (i ma wiele przykładów i opisów działania różnych zaimplementowanych funkcji) to praca z nim okazała się bardzo przyjemna.

niedziela, 8 lipca 2012

Największe zło to ludzie zarabiający na Youtube!

Tak jakoś ostatnio przez "polski YouTube" przelewa się fala opowieści o tym ile to zarabiają sławetni "jutuberzy" na swoich widzach. Cegiełek w tej wielkiej opowieści można by naliczyć bez liku, toteż nie będę przytaczał ich tutaj wszystkich.... w zasadzie wstawię jedynie przeróbkę sławetnej sceny z "Upadku" :)


A jak ktoś jest bardzo zainteresowany tematem to może poszukać po Youtubach tych sławetnych filmów... (zamieszczam również kilka linków dla leniwych: [link] [link] [link]).

No ale o co chodzi... jak nie wiadomo o co chodzi, to chodzi o kasę - a konkretniej o magiczne stawki za wyświetlenia filmików. Jako, że mam dobre serce i nie potrafię zdzierżyć tego, że ludzie muszą ręcznie obliczać ile ktoś zarobił na danym filmiku - przygotowałem magiczne narzędzie w formie skryptu do greasemonkey o wdzięcznej nazwie "YouTube Profit Approximator", czyli z polskiego "Aproksymator zysków z YouTuba", którego jedynym (ale jakże ważnym) celem jest przybliżenie zarobków z danego filmiku.

Przykład działania skryptu
Oczywiście nie radzę podchodzić do tego narzędzia jako ostatecznej wyroczni przychodów, a raczej jako pewnego satyryczno-statystycznego ujęcia całej kwestii związanej z zarobkami na YouTube.
Od strony kodu owego skryptu sprawa wygląda dość prosto:
function ypa_main()
{
 var view = document.getElementsByClassName('watch-view-count')[0].getElementsByTagName('strong')[0].innerHTML;
 view = new Number(view)
 
 var info = document.createElement("span");
 info.title = "Projected earning";
 info.innerHTML = Math.floor((view*4.5)/10)/1000 + "$";
 
 document.getElementById('watch-actions-right').insertBefore(info, document.getElementsByClassName('watch-view-count')[0]);
}

Po wyciągnięciu liczby odwiedzin przy danym filmiku obliczamy ilość "zarobionych dolarków" wg wzoru 100k odwiedzin = 45 dolarów (wartość podana przez RockAlone2k na ubiegły rok). Należy oczywiście pamiętać, że tu estymowane są jedynie zarobki pochodzące z wyświetleń, co oznacza, że zupełnie nie braną są pod uwagę przychody z "klikniętych" reklam.

Tak więc zapraszam do pobierania mego skryptu i szukania tych złych grabieżców polskiej sceny YouTubowej - http://userscripts.org/scripts/show/138011

niedziela, 1 lipca 2012

Dithering i C# (PGM)

Niecały rok temu opublikowałem wpis o ditheringu, w którym w zasadzie nic prócz pokazania efektów tego cudu "nowoczesnej" technologii nie pokazałem. Dziś zamierzam to naprawić poprzez:
  • udostępnienie biblioteki, wykonującej obsługującej proces ditheringu (póki co na plikach PGM) przy użyciu metod:
    • Random
    • Threshold
    • Ordered
    • Floyd-Steinberg
    • "fałszywy" Floyd-Steinberg (czasem opisywany jako Floyd-Steinberg - stąd "fałszywy")
    • Jarvis-Judice-Ninke
    • Stucki
    • Burkes
    • Sierra
  • zgrubny opis owych metod wraz z przykładowymi efektami ich działania
  • W najbliższym zaś czasie dorzucę kilka własnych metod będących mniej lub bardziej efektem mych eksperymentów :)

Biblioteka

Obsługa ditheringu została dodana jako statyczna klasa (Transform.Dither) biblioteki Runaurufu.Drawing (do pobrania z chomika) i w obecnym kształcie zawiera metody:
public static class Dither
{
    public static PgmImage Threshold(PgmImage img, UInt16 newMaxColor){...}
    public static PgmImage Random(PgmImage img, UInt16 newMaxColor){...}
    public static PgmImage Ordered(PgmImage img, UInt16[] box, UInt16 newMaxColor){...}
    public static PgmImage Ordered2(PgmImage img, UInt16 newMaxColor){...}
    public static PgmImage Ordered3(PgmImage img, UInt16 newMaxColor){...}
    public static PgmImage Ordered4(PgmImage img, UInt16 newMaxColor){...}
    public static PgmImage Ordered8(PgmImage img, UInt16 newMaxColor){...}
    public static PgmImage Ordered16(PgmImage img, UInt16 newMaxColor){...}
    public static PgmImage FloydSteinberg(PgmImage img, UInt16 newMaxColor){...}
    public static PgmImage FalseFloydSteinberg(PgmImage img, UInt16 newMaxColor){...}
    public static PgmImage JarvisJudiceNinke(PgmImage img, UInt16 newMaxColor){...}
    public static PgmImage Stucki(PgmImage img, UInt16 newMaxColor){...}
    public static PgmImage Burkes(PgmImage img, UInt16 newMaxColor){...}
    public static PgmImage Sierra3(PgmImage img, UInt16 newMaxColor){...}
    public static PgmImage Sierra2(PgmImage img, UInt16 newMaxColor){...}
    public static PgmImage Sierra24A(PgmImage img, UInt16 newMaxColor){...}
}

Metody Ordered2 - Ordered16 wywołują metodę Ordered z wykorzystaniem predefiniowanych tablic kwadratowych o boku równym numerowi znajdującemu się przy nazwie metody (Ordered2 - tablica 2x2).

Metody Threshold i Ordered wykorzystują zrównoleglenie (Parallel.For) - niestety metody z propagacją błędu nie są w stanie z daru wielowątkowości skorzystać.

Opis metod

Oryginalny obraz Oryginalny obraz w odcieniach szarości
Nie będę się tu zagłębiał w to po co powstał dithering, ani czym jest - bo na pierwsze pytanie można napisać rozprawę naukową i dość dobry ogląd sytuacji przedstawia nam choćby Wikipedia, a na pytanie czym jest odpowiedź znaleźć można w poniższym opisie wybranych metod. Dodatkowo dla porównania załączyłem obrazy będące wynikiem działania każdego z algorytmów na obrazie przykładowym złożonym z 4 elementów (w tle gra Civilization V, a z przodu przykładowe pingwiny z Windowsa oraz popularne obrazy - Lena i rzeźba Dawida dłuta Michała Anioła):


Threshold

Threshold dithering
Jedna z prostszych metod ograniczania ilości kolorów obrazu. Polega na rzutowaniu wartości mniejszych od pewnego wybranego progu (ang. threshold - próg, wartość progowa) na jedną zadaną wartość, a wartości większych lub równych na wartość inną (np. 0 i 255). Zazwyczaj wartość progu określa się jako medianę dostępnych wartości (128 = 256/2). Metoda ta ma tą zaletę, że jest szybka i bardzo łatwa do zrównoleglenia. Niestety wiele do życzenia pozostawia efekt końcowy w przypadku zdjęć o zróżnicowanej kolorystyce.






Random

Random dithering
Kolejna prosta metoda, która tym razem wykorzystuje losowo generowaną wartość progową (losowe wartości mogą znajdować się w takim samym zakresie jak wartości dla poszczególnych pikseli). Dla każdego piksela próg jest generowany osobno, co redukuje możliwość powstawania różnego rodzaju powtarzających się wzorów, jednak z tego samego powodu nie ma praktycznej możliwości dwukrotnego wygenerowania takich samych obrazów bez zapisania również wszystkich użytych progów.


Ordered

Ordered dithering ze wzorem 16x16
Metoda uporządkowanego ditheringu jest jedną z metod związanych z używaniem pewnych wzorów, które "nakłada się" na obraz poddawany obróbce. W tym przypadku wartości progowe pochodzą właśnie z owego wzoru, który powtarza się tak horyzontalnie jak i wertykalnie - stąd też biorą się podobnie rozlokowane artefakty. Zaletą algorytmu jest łatwość zrównoleglenia, która w połączeniu z całkiem dobrymi rezultatami wizualnymi tej metody okazuje się idealnym rozwiązaniem w przypadku wielordzeniowych systemów.




Floyd-Steinberg

Floyd-Steinberg
Z metod związanych z propagacją błędu omówię jedynie metodę Floyda-Steinberga. Dlaczego? A z tej prostej przyczyny, że pozostałe (a mówiąc wprost - pochodne) różnią się wyłącznie pozycjami i wartościami ułamku błędu.
Metoda ta opiera się na wykryciu błędu między wartością piksela (konkretnych kolorów w pikselu) oryginalnego obrazu, a wartością w obrazie przetworzonym.
Opis propagacji błędu
w metodzie Floyda-Steinberga
gwiazdką (*) oznaczono
aktualnie przetwarzany piksel
Owa różnica - błąd kwantyzacji - jest następnie rozdzielana wg zdefiniowanej tablicy (zaprezentowanej obok) na sąsiednie nieprzetworzone piksele. Wadą tej metody jest kompletna niezdolność do zrównoleglenia - algorytm musi przejść piksel po pikselu - która jednak jest niejako zacierana przez niesamowite efekty uzyskiwane dzięki tej metodzie. Za konceptem propagacji błędu stoi proste spostrzeżenie - zarówno wartość 0 jak i 120 zostaną zrzutowane na 0, co sprawia, że obraz z szarego staje się czarny. Jeśli jednak mamy 3 kolejne piksele o wartości 120, to wynikiem pracy tego algorytmu nie będzie ciąg {0,0,0}, ale ciąg {0, 255 (120 + 52.5), 0 (120 - 82.5)}, który o wiele lepiej prezentuje szarość.
Stucki dithering
Pochodne metody różnią się jedynie wartościami, liczbą i pozycją ułamków błędu kwantyzacji.

Bibliografia

Czyli co warto przeglądnąć w poszukiwaniu informacji o ditheringu...
  • Wikipedia - czyli dobry wstęp do rozpoczęcia przygody z tą tematyką
  • Praca Lee Daniela Crockera (rozszerzona przez Paula Boulaya oraz Mike Morra) - opisuje dobrze zarówno historię całej idei ditheringu jak i wyżej omawiane algorytmy. (mirror)
  • Joel Yliluoma's arbitrary-palette positional dithering algorithm - strona z przykładami i kodami źródłowymi w PHP. Przydatna do porównania różnych algorytmów - również przy zastosowaniu w filmie.
  • Image Based Artistic Dithering - praca  studentów z PUC w Rio, może nie odkrywcza, ale jeśli ktoś zaczyna przygodę z tą tematyką to może się przydać (choć jak ktoś faktycznie chce ugryźć temat to o wiele lepiej zacząć od pracy Lee Daniela Crockera)

czwartek, 14 czerwca 2012

Obsługa formatu Netpbm przez C#

 Wstęp teoretyczny

Format Netpbm odnosi się tak na prawdę do zestawu formatów plikowych służących do przechowywania obrazów. W skład tego pakietu wchodzą trzy podstawowe formaty:
  • PBM - Portable Bit Map
  • PGM - Portable Gray Map
  • PPM - Portable Pixel Map
 Do tego czasem dorzuca się PAM (Portable Arbitrary Map), którego struktura opiera się na zasadach wręcz identycznych do tych odnoszących się do PBM/PGM/PPM (które od teraz zbiorczo będę określał jako PNM - Portable Any Map).
Jeśli kogoś interesuje więcej na temat tego formatu to zapraszam do czytania:
A jeśli nie chcecie zagłębiać się w te techniczne detale (bo i po co?) to potraktujmy ten wstęp teoretyczny za zakończony i przejdźmy do sedna...

niedziela, 3 czerwca 2012

Android custom spinner

Przygody z Androidem doprowadziły mnie do momentu, w którym zmuszony zostałem zabawić się w tworzenie własnych elementów wyświetlanej listy. Oczywiście ani stworzenie własnego layoutu, ani adaptera nie jest czymś wielce trudnym... ale potrafi dostarczyć pewnych niemiłych doznań podczas procesu tworzenia. Generalnie problemem, który sprawił, że ten wpis powstał było zadanie generowania innego layoutu (w widoku dropdown) dla elementu obecnie zaznaczonego przez spinner.

Przykład, który tu zostanie przedstawiony można pobrać z chomika: (link)

niedziela, 20 maja 2012

UFO Afterlight

Kilka dni temu nabyłem UFO Afterlight po promocyjnej cenie 2zł (cała opowieść znajduje się tutaj) i tak się ucieszyłem z tej transakcji, że postanowiłem od razu go zainstalować :) Instalacja przebiegła bez problemów, więc tutaj zastrzeżeń nie mam. I na tym by się skończyła radość ma z bezproblemowego uruchamiania tej gry :(


Na początek ujrzałem magię paranoicznych zabezpieczeń, więc bez ociągania się rozpocząłem poszukiwania sposobu na obejście konieczności instalowania tego "przyjaznego użytkownikowi" oprogramowania. Efektem kilku minut poszukiwań były aż dwa patche (po instalacji dostajemy wersję 1.4)... 1.5 do pobrania ze strony Cenegi (link | mirror) oraz 1.7 ze strony twórców gry - studia ALTAR Games (link | mirror). Co ciekawe 1.7 jest przeznaczony dla wszystkich edycji (w tym dystrybucji cyfrowej) i generalnie nie posiada i nie wymaga żadnych uroczych zabezpieczeń typu STARFORCE.

Tak więc grę zaktualizowałem, wszystko pięknie (notabene wersja 1.7 dodaje menedżer językowy, więc bez trudu można zmieniać język gry :), aż do momentu próby odpalenia gry. Wtedy to pojawia się błąd o braku dllki...

Na szczęście proces patchowania nie usunął tego pliku, a jedynie zmienił jego nazwę na OpenAL32._dll - tak więc nazwę zmieniamy na OpenAL32.dll i już bez dalszych trudów możemy uruchamiać grę :D



Co jednak nie znaczy, że bez dalszych trudów się obędzie. Na mej drodze stanęła o zgrozo rozdzielczość mego monitora w laptopie (1366x768), która to okazała się niewystarczająca pod względem wysokości. Konkretniej część ekranu gry po prostu nie renderowała się i tak ekran ustawień zamiast wyglądać tak:
wyglądał tak (nie miał m.in. guzików umożliwiających zmianę ustawień):

I żeby nie było, to te guziki były "renderowane", tylko poza ekranem - na co dowód dostarczył podgląd okna Windowsa :P

Na szczęście remedium na ten problem jest bardzo proste - wystarczy otworzyć grę w oknie (odpowiedni skrót z menu start), zmienić rozdzielczość na taką jaka nam odpowiada, a następnie możemy już uruchamiać grę na pełnym ekranie i będziemy widzieć wszystkie elementy gry ^.^ (poza tym okienkowy tryb gry automatycznie skaluje zawartość gry do rozmiaru okna, więc w oknie zawsze widzimy wszystko).

niedziela, 29 kwietnia 2012

WebBrowser i cieknąca pamięć

Jeśli kiedykolwiek używaliście domyślnego WebBrowsera w C# to za pewne zauważyliście, że ilość pamięci zużywanej przez wasz program rośnie wraz z użytkowaniem tej kontrolki w zasadzie w nieskończoność. Dlaczego tak się dzieje? Cóż dokładnie nie wiadomo, wiadomo jednak, że jest to spowodowane głównie cachowaniem zawartości odwiedzonych stron... dodajmy cachowaniem, którego nie można w całości wyłączyć/wyczyścić. Oczywiście można by sądzić, iż problem ten wynika z leciwości tej kontrolki, jednak ku memu wielkiemu zaskoczeniu to samo dzieje się z WebBrowserem dostępnym wewnątrz WPF (obie kontrolki używają tego samego silnika IE).
Ale po co w ogóle używać kontrolkę WebBrowser? Poza oczywistym zastosowaniem - umożliwienia "przeglądania" Internetu (klikania w łącza, oglądania obrazków, czytania tekstów) można również używać go do parsowania ściągniętych plików HTML, z którymi natywne parsery .NET nie dają sobie rady z racji niezgodnego ze standardem użycia tagów.

Oczywiście to do czego chcemy używać WebBrowser jest sprawą drugorzędną, gdyż wpływa jedynie na to czy istnieje jakieś rozwiązanie alternatywne, czy też skazani jesteśmy na WebBrowsera, który pożre przy sprzyjających dla siebie warunkach wszelkie ilości dostępnej pamięci nie dając w zamian absolutnie nic. W tym wpisie przedstawię kilka sposobów na ograniczenie ilości wyciekającej pamięci, więc jeśli ktoś liczy na to, że znajdzie tu sposób na całkowite zażegnanie tego problemu to spotka go rozczarowanie, gdyż takie sposobu nie znam, ani nigdzie nie mogłem znaleźć... Obalę również kilka mitów krążących po sieci związanych z "magicznymi sposobami" na ograniczenie używanej pamięci. Oczywiście zawsze można zaprzestać używania natywnych kontrolek i wykorzystać kontrolki stworzone w pocie czoła innych programistów (dla porównania w dalszej części przedstawię właśnie kontrolkę Awesomium):

wtorek, 27 marca 2012

Jak sprawdzać istnienie używanych elementów?

Pytanie brzmi "jak", bo to, że jakoś weryfikować trzeba to mam nadzieję jest oczywiste :) Generalnie rozchodzi się o kilka sytuacji - np. gdy chcemy zinkludować jakiś plik, lub czytamy tablicę i nie jesteśmy pewni, czy jakiś określony klucz jest w niej ustawiony (choćby w przypadku $_POST/$_GET). Oczywiście PHP jako ten nowoczesny język udostępnia nam kilka możliwości sprawdzenia czy faktycznie mamy co czytać, toteż w tym wpisie spróbuję porównać wydajność tych metod.

Elementy tablicy
Na pierwszy ogień pójdzie sprawdzanie istnienia elementów tablicy. By jednak nie było tak całkiem prosto i by było o czym pisać to założymy, że tablica jest wielowymiarowa, czyli składa się z tablic, które składają się z innych tablic:
$arr = array("Nested" => array("Nested" => array("Nested" => "Here")));
Najpopularniejszym sposobem sprawdzania (i generalnie wpajanym wszystkim od maleńkości) czy dany klucz istnieje, jest wykorzystanie funkcji isset(). Tylko, że możemy ją wykorzystać na wiele sposobów...

if(isset())
Dla każdego przykładu użycia dodałem zarówno wywołanie dla elementu w tablicy się znajdującego (Nested), jak i tego, którego w niej nie ma (Indo).
if(isset($arr['Indo']['Indo']['Indo'])) $str = $arr['Indo']['Indo']['Indo'];
if(isset($arr['Nested']['Nested']['Nested'])) $str = $arr['Nested']['Nested']['Nested'];
if & isset (not exist):    0.016935110092163
if & isset (exist):     0.029613971710205
Jak widać PHP szybciej odkryje, że czegoś nie ma... niż, że coś jest :)

Jeden if, wiele isset()
Wydawać by się mogło, że sprawdzenie całej ścieżki w pojedynczym isset() jest o wiele lepszym rozwiązaniem niż wielokrotne wywołanie funkcji...
if(isset($arr['Indo']) && isset($arr['Indo']['Indo']) && isset($arr['Indo']['Indo']['Indo']))
 {} else {}
if & isset (multi - line LR) (not exist):  0.019056081771851
if & isset (multi - line RL) (not exist):  0.021497964859009
if & isset (multi - line LR) (exist):   0.056113004684448
... i niewątpliwie tak jest.. a na pewno jeśli wszystkie wywołania znajdują się w jednym "if". Dlaczego jednak zamieściłem tu ten przykład? Ano dlatego, że można z niego wynieść ważne informacje:
  • PHP sprawdza warunki od lewej do prawej (Left to Right) i kończy obsługę warunku gdy pierwszy z nich == FALSE
    Zatem taki zapis jest bardziej efektywny (po lewej warunki najważniejsze)
    .if(isset($arr['Indo']) && isset($arr['Indo']['Indo']) && isset($arr['Indo']['Indo']['Indo']))
     {} else {}
    niż zapis taki (najważniejsze warunki po stronie prawej)
    if(isset($arr['Indo']['Indo']['Indo']) && isset($arr['Indo']['Indo']) && isset($arr['Indo']))
     {} else {}
  • Sprawdzenie pojedynczego poziomu tablicy jest szybsze niż sprawdzanie kilu poziomów
    if(isset($arr['Indo']['Indo']['Indo'])) {} else {}
    if(isset($arr['Indo'])) {} else {}
    if & isset([][][])(not exist):    0.022554874420166
    if & isset([]) (not exist):    0.016841888427734
    if & isset([][][]) (not exist):   0.032048225402832
    if & isset([]) (not exist):    0.017351150512695

 if i isset() dla każdego poziomu
Taki zapis jest jakby typowym "kaskadowym podejściem" do tego zagadnienia.
if(isset($arr['Indo']))
  if(isset($arr['Indo']['Indo']))
   if(isset($arr['Indo']['Indo']['Indo']))
   {}
Generalnie zaletą tego podejścia jest to, że możemy wcześniej wykryć nieistniejące elementy i nie musimy przeprowadzać analizy całej tablicy. Jeśli jednak nie zależy nam na wykluczeniu istnienia, a jego udowodnieniu, to sens użycia takiego sposobu jest nikły. Do tego dochodzi konieczność obsługi na każdym poziomie kaskady sytuacji nieistnienia elementu.
if & isset (multi - cascade) (not exist):  0.018480777740479
if & isset (multi - cascade) (exist):   0.068065166473389
isset() ? operacja : operacja
Wykorzystanie tej konstrukcji tak na dobrą sprawę ma sens albo gdy chcemy szybciej napisać kod ("? :" to zdecydowanie mniej znaków niż "if() else"), albo gdy do danej zmiennej zamierzamy przypisać jakąś wartość (np. parsowanie danych wprowadzonych przez użytkowników)
isset($arr['Indo']['Indo']['Indo']) ? null : null;
isset & ? (not exist):     0.018883943557739
isset & ? (exist):     0.031083106994629
Bo jak widać pod względem wydajności metoda ta jest nieznacznie gorsza od starego poczciwego if'a.

@element 
Oczywiście poza isset() istnieje również metoda "empiryczna"... a dzięki operatorowi @ możemy ignorować generowane przez PHP błędy (Notice: Undefined index) związane z nieistnieniem elementu, do którego się odwołujemy.
$str = @$arr['Indo']['Indo']['Indo'];
@ (not exist):      0.16418194770813
@ (exist):      0.09358811378479
strict (exist):     0.030779838562012
Tylko, że jak widać czas związany z obsługą błędu jest o wiele większy niż czas potrzebny na przeszukanie tablicy przy pomocy isset().

Pliki
W przypadku plików również mamy kilka możliwości sprawdzenia czy istnieją.. Jednak tutaj opisze tylko trzy: include, file_exists() oraz is_file().

file_exists()
Funkcja ta generalnie służy do sprawdzenia czy dany plik istnieje.
if(file_exists("empty.php")) {} else {}
file_exists (exist):     0.030549049377441
file_exists (not exist):    0.048150777816772
file_exists include (exist):    0.10239315032959
file_exists ? include (exist):    0.10641098022461
file_exists include (not exist):   0.048465013504028

@include
Tym razem znów skorzystamy ze znanego nam już operatora blokującego informacje o błędach.
$var = @include "empty.php";
@include (exist):     0.20435905456543
@include (not exist):     0.073977947235107
Dlaczego przypisujemy to co zwraca include do zmiennej? Ano dlatego, że include zwracać może dowolne wartości, ale domyślnie (jeśli w includowanym pliku nie dodano returna) zwraca TRUE (jeśli plik załadowano) lub FALSE (jeśli pliku nie udało się załadować).

is_file()
Ostatnią zaprezentowaną tutaj metodą jest użycie funkcji is_file(), która to zwraca TRUE jeśli wskazana przez nas ścieżka jest zwykłym plikiem lub FALSE w każdym innym przypadku (czyli jeśli np. wskazuje na folder lub nie wskazuje na nic).
if(is_file("empty.php")) {} else {}
is_file (exist):     0.00079703330993652
is_file (not exist):     0.064265966415405
To co zaskakuje to szybkość z jaką is_file() działa - ok. 40 razy szybciej wykrywa istniejące pliki. Tak więc jeśli szukamy szybkiego sposobu na sprawdzanie czy pliki istnieją - is_file() jest idealnym wyborem. Niestety jego wykrywanie nie istniejących plików jest wolniejsze niż file_exists() - tak więc w zależności od głównego celu naszego bloku kodu powinniśmy wybrać odpowiednią funkcję.

Słowo na koniec
Tak więc jeśli chcemy sprawdzić czy w tablicy znajduje się dany element - to używamy isset(). Jeśli zaś chodzi o pliki, to w zależności od tego czy głównym scenariuszem użycia danego bloku kodu będzie wykorzystanie informacji o istnieniu lub nieistnieniu pliku powinniśmy użyć odpowiedniej funkcji - jeśli chcemy odczytać dany plik to używajmy is_file(), jeśli zaś chcemy stworzyć nowy plik o podanej nazwie - użyjmy file_exists().

A na koniec jak zwykle zapraszam do pobrania pliku z kodem użytym do testów: (link)

niedziela, 25 marca 2012

PHP a wersje językowe

Tworząc aplikację internetową w PHP prędzej lub później zapragniemy dodać do niej wsparcie dla wielu języków. Oczywiście tak to jak i wiele innych rzeczy można zrobić na wiele sposobów. W tym wpisie przedstawię zaledwie kilka z nich, choć myślę, że są to akurat te bardziej popularniejsze rozwiązania (Google it).
Większość metod tłumaczenia używa różnego rodzaju plików do przetrzymywania informacji o "wyglądzie" tekstu w danej wersji językowej, dlatego też znaczną część wpisy poświęcę omawianiu tych różnych "plikowych tworów".

Na początku był define
A jeśli nawet nie na początku, to jest to droga, która w ten czy inny sposób wydaje się "prosta i oczywista".
define ('Hello_World', "Witaj Świecie");

echo constant('Hello_World');
echo Hello_World;
Oczywiście przy wykorzystaniu takiej metody wszelkie definicje tekstów powinny być umieszczane w osobnym pliku dla każdej wersji językowej i w zależności od potrzeb includowawne.
Co ciekawe odwoływanie się do zdefiniowanej stałej przy użyciu constant() trwa dłużej niż bezpośrednie wywołanie:
pure:    0.5458300113678
with constant:   0.85419201850891

Metoda ta jednak nie sprawdza się w przypadku większych portali, gdzie liczba tłumaczeń przechodzi w setki czy nawet tysiące. Do tego dochodzi całkowity brak wsparcia po stronie IDE (brak auto uzupełniania i innych cudów techniki).

A gdyby tak tablicę
Co prawda tablica sama w sobie również wsparcia od IDE nie oferuje, ale daje większe możliwości segregacji wpisów (np. poprzez tworzenie wielu wymiarów).
define:   0.4133129119873
define with constant:  0.88955783843994
array:    0.35322785377502
multi array:   0.42432999610901
Poza tym jak widać po osiąganych czasach wykorzystanie tablicy okazuje się lepszą metodą niż definiowanie stałych (co zaskakujące nie jest) i dopiero wykorzystanie wielowymiarowych tablic zbliża te czasy do siebie.

.ini czyli coś prawie zapomnianego 
Dziś już prawie nikt nie używa plików .ini - bo i po co skoro są choćby XML.. Jednak ogromną zaletą plików .ini jest ich mały narzut związany z obsługą danych - nie potrzeba wieloznakowych tagów zarówno otwieranych, jak i zamykanych... nie - wystarczy klucz i znak '='. Czego rezultatem jest zaskakująca wydajność (czas liczony łącznie z parsowaniem pliku!):
define:   0.43395209312439
define with constant:  0.94658613204956
array:    0.36954712867737
multi array:   0.44773697853088
ini array:   0.42291903495789
Do tego PHP zapewnia wbudowany parser plików .ini - parse_ini_file() - tak więc stworzenie translacji na tej zasadzie jest dość proste. Poza tym pliki .ini są dość popularnym sposobem trzymania danych konfiguracyjnych dla np. bazy danych :)
Minusem wbudowanego parsera jest jednak fakt, że wygenerowana tablica może mieć maksymalnie 2 wymiary.

Profesjonalny gettext
W świecie PHP oczywiście istnieją również rozwiązania dedykowane dla lokalizacji - w tym przypadku jest to zbiór funkcji, do których generalnie odnosi się poprzez słowo "gettext". Funkcje te tak naprawdę dostępne są w wielu różnych językach programowania albo dzięki natywnym bibliotekom, albo dzięki pracy zwykłych "szarych" użytkowników. Zaletą gettexta jest jego profesjonalne podejście do zagadnienia - podczas pracy z nim dostajemy pliki z jasno opisanymi wystąpieniami każdego elementu, który mamy przetłumaczyć. No właśnie - tutaj tak naprawdę specjalny "szperacz" wyszukuje frazy, które mamy przetłumaczyć, więc możemy spokojnie pisać i testować kod nie obawiając się, że nagle dostaniemy błąd o braku danego wpisuj w tablicy czy nieistnieniu jakiejś zmiennej.

Jeśli przypadkiem poszukujecie kompilatra dla gettext to polecam zajrzeć na http://savannah.gnu.org/projects/gettext.

Natywny gettext
Wbudowany gettext wymaga zainstalowania (i uruchomienia) na serwerze rozszerzenia php_gettext.
define:    0.030115127563477
define with constant:   0.061676025390625
array:     0.021489143371582
multi array:    0.027302980422974
ini array:    0.02887487411499
Czas dla gettext (native):  0.87748694419861

Porównując szybkość jego działania jednak szybko dojdziemy do wniosku, że wcale nie jest to taki wyśniony wydajnościowo sposób implementowania lokalizacji. Cóż za to jest on na pewno przyjemniejszy podczas pisania kodu i samego procesu translacji.

gettext dla ubogich 
Może nie koniecznie dla ubogich w sensie dosłownym, a raczej dla ubogich pod względem możliwości serwera. Natywny getext ma to do siebie, że ładuje pliki z tłumaczeniami raz do pamięci i trzyma je generalnie tam aż do restartu serwera - owszem istnieją metody na obejście tego.. ale jeśli ktoś zajmuje się głównie tworzeniem strony i generalnie deweloperką to raczej będzie skłonny poświęcić trochę wydajności podczas testów na rzecz prostszego i szybszego w napisaniu kodu.Do tego dochodzi problem z aktualizacją słownika w przypadku serwerów współdzielonych - mało który admin zgodzi się na restart tylko dlatego, że nasz słowniczek zmienił tłumaczenia dla kilku elementów...
define:    0.031126976013184
define with constant:   0.062520980834961
array:     0.021286010742188
multi array:    0.02723503112793
ini array:    0.027822017669678
Czas dla gettext (native):  0.90787696838379
Czas dla gettext (custom):  1.8490700721741

Tak więc jeśli chcecie wypróbować tą nieoficjalną implementację to zapraszam na https://launchpad.net/php-gettext.

Sposoby z kosmosu
No może nie całkiem z kosmosu, bo czasami inaczej się po prostu nie da (lub wykorzystanie plikowych sposobów nie ma sensu). Ale jeśli ktoś opiera swoją wielojęzyczność elementów statycznych na bazie danych to znak, że coś się dzieje... By była jasność nie chodzi mi o elementy, które użytkownicy mogą dynamicznie dodawać do portalu, ale o tłumaczenia np. strony do logowania ^.^ Generalnie wszelkie odwołania do zewnętrznych zasobów (a baza danych takowym jest - no chyba, że mówimy o serwerze PHP znajdującym się na tej samej maszynie co baza, która to jeszcze trzyma wszystkie dane w pamięci podręcznej) to utrata cennego czasu i zasobów. Oczywiście można użyć tłumaczy online takich jak Google translate, altavista (Babel fish), czy bing translator... ale to trwa i wykorzystanie takich pereł polecałbym przenieść do JavaScriptu odpalanego przez klienta i to tylko i wyłącznie dla treści, których tłumaczenia po prostu nie jesteśmy w stanie dostarczyć.
Ciekawym sposobem podejścia do tematu jest wykorzystanie klas i zawartych w nich zmiennych jako kontenerów na tłumaczenia. Takie podejście ma ten plus, że zapewni nam auto uzupełnianie IDE i łatwość ładowania innych wersji językowych. Wadą jest wydajność, gdyż narzut związany z tworzeniem klasy powoduje, że czas konieczny na obsłużenie tego typu lokalizacji jest ok. 2 razy większy niż w przypadku deklarowania stałych za pomocą define().

Puenty brak
Cóż czasy mówią same za siebie, tak więc jeśli zależy wam na wydajności to z pewnością pomyślicie nad jakimś własnym rozwiązaniu opartym o pliki (dla leniuszków polecam zapoznać się z plikami .ini gdyż są bardzo wdzięczne pod względem tak możliwości łatwej translacji jak i użytkowania w aplikacji). Jeśli jednak już zamierzacie zagłębiać się w tajniki gettexta - technologia ta jest bardzo denerwująca (pomijając już fakt konieczności kompilacji, mergowania tłumaczeń...) choćby z tego powodu, że zwykle ciężko skłonić natywną wersję do poprawnego działania (wersja ~danilo zdecydowanie jest łatwiejsza w obsłudze) o wydajności nie mówiąc...

Chcących zaglądnąć w kod odsyłam do swojego chomika: (link)

poniedziałek, 12 marca 2012

Gdy program nie widzi sieci Hamachi

Dość częstym problemem w przypadku łączenia się z programami uruchomionymi na innych maszynach znajdujących się w sieciach Hamachi jest to, że wszelkie "auto wyszukiwarki" serwerów po prostu takowych komputerów nie uwzględniają. Innymi słowy - o ile często łączenie się po podaniu IP działa bez problemu, to już różnego rodzaju narzędzia pokazujące dostępne serwery po prostu nigdy do sieci Hamachi nie dochodzą. W rezultacie pojawia się taki problem, że np. gdy chcemy podłączyć się do serwera jakiejś gry postawionego u naszego kolegi - to niestety nie mamy takiej możliwości (chyba, że uda się nam podłączyć bezpośrednio po IP).

Dlaczego tak się dzieje? Generalnie Hamachi po instalacji dodaje swój adapter sieciowy na koniec listy połączeń zarejestrowanych w systemie. Samo to w sobie jest akurat zachowaniem pożądanym. Problem pojawia się gdy aplikacja wyszukująca serwer używa tylko i wyłącznie pierwszego połączenia z listy, czyli odnosi się do nadanego nam adresu IP związanego z tym właśnie połączeniem.

Czy da się to jakoś zmienić? Na nasze szczęście tak :)

W celu zmiany kolejności połączeń musimy udać się do naszego Panelu sterowania i w zależności od używanego widoku wybrać "Wyświetl stan sieci i zadania" lub "Centrum sieci i udostępniania".

Następnie wybieramy "Zmień ustawienia karty sieciowej" (na listwie po lewej stronie).

A teraz pora na odrobinę magii.. W otwartym oknie powinna pojawić się nam lista dostępnych połączeń sieciowych, jednak to co nas interesuje to menu, które pokazuje się dopiero po naciśnięciu klawisza [ALT]. W menu tym wybieramy "Zaawansowane" -> "Ustawienia zaawansowane...".

W nowym oknie powinniśmy ujrzeć listę połączeń, a na niej połączenie Hamachi (1). Klikamy więc na połączenie Hamachi, a następnie klikamy na strzałkę skierowaną ku górze (2) tak długo, aż to połączenie nie znajdzie się na szczycie listy. Klikamy OK i gotowe :)
Możliwe, że do uzyskania odpowiedniego efektu konieczne będzie ponowne uruchomienie komputera, ale generalnie ten zabieg powinien rozwiązać problem z niewykrywaniem serwerów :D

niedziela, 5 lutego 2012

Zabawa z Windows (Live) Movie Maker

Windows Movie Maker jest przyjaznym programikiem do tworzenia filmów nawet przez osoby całkiem zielone w tym temacie. To czym jednak zajmiemy się w tym wpisie, to nie tyle tworzenie filmów, co tworzenie konfiguracji użytej przy konwersji naszego dzieła do finalnej postaci. Ale o co chodzi? Cóż w najprostszym ujęciu chodzi o redukcję rozmiaru filmu przy minimalnej redukcji odczuwalnej jakości obrazu (odczuwalnej, gdyż ta realna cierpi bardzo).

Na sam początek kilka przykładów tego na co pozwalają opcje konfiguracji WMM. Do tego pokazu użyłem małej składanki nagrań z gry StarCraft II, a rezultaty przedstawiam poniżej w tabeli (polecam podglądanie filmików na pełnym ekranie i w "jakości" 720p [tzw. HD], by móc dokładniej ocenić jakość efektu końcowego).

Jakość kompresji filmuRozmiarPodgląd
Oryginał ~7000 kb/s81.0 MiB
6000 kb/s75.5 MiB
5000 kb/s65.7 MiB
4000 kb/s54.3 MiB
3000 kb/s42.3 MiB
2000 kb/s30.2 MiB
1000 kb/s17.9 MiB
500 kb/s11.5MB
200 kb/s7.62 MiB
100 kb/s6.31 MiB

Zauważyliście, że wszystkie te filmy dostępne są w "HD"? Cóż to wynika z tego, że YouTube uznaje za "HD" wszystkie filmy o wysokości powyżej 720 pikseli. Ponad półtorej minuty HD zajmujące 6MiB? Mniami ;]

To nad czym warto się tutaj zastanowić (a co w zasadzie jest jednym z powodów powstania tego wpisu) to to czy warto wysyłać filmy w tak wysokiej jakości (6k lub więcej kb/s) na YouTube skoro one i tak są potem kompresowane do formatu mp4. Rezultatem tej kompresji jest to, że finalny odbiorca nie widzi tak naprawdę wielkiej różnicy w jakości... Rozmyślanie to oczywiście dotyczy głównie sytuacji gdy mamy słabe łącze (niski limit uploadu) ograniczające możliwość szerzenia naszej twórczości, bo jeśli wrzucenie 1GiB filmu zajmuje nam czas np. ok. 10min  to zaprawdę taka zabawa z kompresją mija się z celem ;]

Ale przejdźmy do sedna - czyli jak sprawić by szybko (bo WMM działa bardzo szybko w porównaniu do innych programów - w tym komercyjnych kosztujących grubą kasę) zredukować rozmiar filmu?

Wybieramy z menu programu opcję "Zapisz film", a następnie "Utwórz ustawienia niestandardowe". Pojawi się nam nowe okienko zaprezentowane poniżej.
Po ukończeniu konfiguracji z tego samego menu będziemy mogli użyć zdefiniowanej konwersji do zapisu naszego filmu (użyte przeze mnie konfiguracje to właśnie te widoczne 1280x720 - xk)

To okno pozwala nam zdefiniować konfigurację tworzonego filmu - efekt działania kompresji WMM.
Opcja która ma dla nas największe znaczenie to "Szybkość transmisji bitów". Wartości powyżej 6k są na Youtubie w zasadzie uznawane za HD, choć np. efekt działania Frapsa w przypadku SC2 to powyżej 370Mb/s (ok 15MiB na sekundę filmu). Drugą ważną wartością jest Format audio, choć jego wpływ jest minimalny na rozmiar pliku (ok. 3MiB różnicy na minutę filmu pomiędzy skrajnymi ustawieniami formatu).

Użyte profile dla WMM do stworzenia filmów pokazanych w tym wpisie można znaleźć na moim chomiku: (link)

środa, 1 lutego 2012

X-COM na Steamie / DOSBox

Jeśli zdarzyłoby się, że ktoś byłby tak wielkim nieszczęśnikiem i zakupił trylogię X-COM poprzez Steam to cóż może czuć się po dwakroć.. a nawet trzykroć wykorzystany. Dlaczego?
  • Brak wersji polskiej dla oryginalnej serii X-COM
  • DOSBox w wersji 0.72 (na którym X-COM Apocalypse lubi się ciąć, wieszać i wyprawiać różne dziwne akcje)
  • Konfiguracja DOSBoxa uniemożliwiająca normalną grę
Na pierwszy punkt wiele poradzić nie mogę - mogę jedynie napisać, że istnieją tłumaczenia na język polski do wszystkich części klasycznego X-COM (przy czym do TFTD i X-COMA oficjalne wypuszczone jeszcze przez Microprose).

Zażegnanie problemowi z punktu drugiego jest w zasadzie banalne - wystarczy ściągnąć nowszą wersję DOSBoxa, zainstalować i zastąpić nią zawartość katalogu z grą.

Trzeci punkt wymaga z kolei edycji pliku konfiguracyjnego DOSBoxa (lokalizacja pliku konfiguracyjnego) i zamiany paru rzeczy:
  • X-COM Ufo Defense / UFO: Enemy Unknown
    Konfiguracja domyślnaKonfiguracja po zamianie
    [cpu]
    cycles=auto limit 8000
    [cpu]
    cycles=auto limit 100000
  • X-COM Terror From The Deep
    Konfiguracja domyślnaKonfiguracja po zamianie
    [cpu]
    cycles=auto limit 16000
    [cpu]
    cycles=auto limit 100000
  • X-COM Apocalypse
    Zmiana w przypadku X-COM Apocalypse ma inne podłoże niż te w przypadku poprzedniczek - tu problemem nie jest za wolna maszyna wirtualna, ale sposób wyświetlania obrazu. Więcej porad dotyczących X-COM Apocalypse i uruchamiania tej gry można znaleźć w innym wpisie na moim bloku: X-COM Apocalypse i Windows 7.
    Konfiguracja domyślnaKonfiguracja po zamianie
    [sdl]
    output=surface
    [sdl]
    output=overlay
Jeśli zaś nie podoba wam się sposób skalowania i chcielibyście by był bardziej zbliżony do oryginału (bardziej pikselowaty, a nie rozmyty) to polecam również zastosować tą zamianę:
Konfiguracja domyślnaKonfiguracja po zamianie
[render]
scaler=advinterp2x
[render]
scaler=normal3x




sobota, 21 stycznia 2012

Nie dla ACTA

Nie będę się tu rozpisywał nad swoim stanowiskiem dotyczącym ACTA, gdyż zrobiłem już to tutaj. W tym poście nawiążę do skryptu na blogi i inne strony, który ma "zaciemnić stronę w ramach protestu". Mowa tu oczywiście o inicjatywie Nie dla ACTA i podstronie dotyczącej samego blackoutu.

Skrypt, który jest tam zamieszczony działa... tyle, że musimy faktycznie bezpośrednio do sekcji "body"ów skrypt wkleić, bo jeśli używamy różnego rodzaju "gadżetów" dodających dynamicznie zawartość (np. HTML/JavaScript w Blogspocie) to komunikat i "zaciemnienie" doda nam się tylko w owym wygenerowanym obiekcie.

Jako, że strona blackoutu zniknęła w tym całym zamieszaniu to dorzucam lekko zmodyfikowany skrypt linkujący do strony fundacji panoptykon
<script type="text/javascript">var d=new Date();if(d.getDate()<=26 && d.getMonth()==0 && d.getFullYear() == 2012){var p="<div style='padding-top:150px;opacity:0.95;z-index:9999;position:fixed;top:0px;left:0px;width:100%;height:100%;background-color:#000000;font-family:Helvetica, Arial, sans-serif;color: #d7d7d7;text-align:center;'>"+" <h1 style='font-size:66px;color: #d7d7d7;'>Nie dla <strong style='color: #d30000;'>ACTA !</strong></h1>" +" <h2 style='color: #d7d7d7;font-size:45px;line-height:50px;'>Nie dla <strong style='color: #d30000;'>cenzury internetu !</strong></h2>"+" <a href='http://www.panoptykon.org/wiadomosc/nie-dla-acta-akcja-protestacyjna' style='color:#d7d7d7; text-decoration:none;'>[ Dowiedz się więcej ]</a>"+"</div>";document.getElementsByTagName("Body")[0].innerHTML=document.getElementsByTagName("Body")[0].innerHTML+p;}</script>

A tutaj wersja z linkiem pozwalającym na powrót do przeglądania naszej witryny
<script type="text/javascript">window.addEventListener('load', function(){var d=new Date();if(d.getDate()<=26 && d.getMonth()==0 && d.getFullYear() == 2012){var p="<div style='padding-top:150px;opacity:0.97;z-index:9999;position:fixed;top:0px;left:0px;width:100%;height:100%;background-color:#000000;font-family:Helvetica, Arial, sans-serif;color: #d7d7d7;text-align:center;'>"+" <h1 style='font-size:66px;color: #d7d7d7;'>Nie dla <strong style='color: #d30000;'>ACTA !</strong></h1>" +" <h2 style='color: #d7d7d7;font-size:45px;line-height:50px;'>Nie dla <strong style='color: #d30000;'>cenzury internetu !</strong></h2>"+" <a target='_blank' href='http://panoptykon.org/wiadomosc/nie-dla-acta-akcja-protestacyjna' style='color:#d7d7d7; text-decoration:none;'>[ Dowiedz się więcej ]</a>"+" <a style='font-size:10px;display:block;margin:50px;' href='#' onclick='this.parentNode.parentNode.removeChild(this.parentNode);'>[ Ukryj apel i przeglądaj stronę ]</a>"+"</div>";document.getElementsByTagName("Body")[0].innerHTML=document.getElementsByTagName("Body")[0].innerHTML+p;}}, false);</script>
Oraz wersja łatwiejsza do kopiowania:


Tak więc jeśli posiadasz własną stronę lub blog - przyłącz się do akcji "zaciemnienia" "polskiego" internetu.
Jeśli zaś strony nie posiadasz - podpisz petycję online: (link), to nic nie kosztuje, a może pomóc.

Oba skrypty dodatkowo "zabezpieczyłem" by odpalały się do 26 I 2012 włącznie... tak więc jak ktoś zapomni usunąć to za rok mu się strona nie zaciemni ;)
Tak poza tym to taka forma zaciemnienia działa jedynie w przypadku gdy przeglądający naszą stronę ma włączoną obsługę JavaScript ;)