Zliczanie pixeli danego koloru (lub przedziału kolorów) w bitmapie

0

witam wszystkich!
bardzo proszę o pomoc w takim temacie:

jest bitmapa, zawsze rozmiaru 228x114, 32-bitowa z zawartością tła (czerwone), narożniki (jakiś fiolet) i częścią która mnie interesuje czyli takim wąskim paskiem (gama kolorów)
chciałbym dowiedzieć się, jak mógłbym zliczać pixele z tego właśnie obszaru w kolorze różnym od tła minus wartości dla żółtego (byłaby to powiedzmy wartość graniczna)
domyślam się, że znając DPI dla danego systemu, jestem w stanie (przynajmniej z grubsza) określić wartość pola powierzchni tego interesującego mnie obszaru.

title przyklad

w trybie automatycznym:

  • wybór, czy też skan w punkcie gdzieś z boku, koloru tła (może się bardzo delikatnie różnić) i koloru narożników (te zawsze będą w kolorze jednolitym)
  • zamiana narożników na kolor tła (choć pewnie nie będzie to konieczne, ale to nie są proste figury geometryczne, więc może jednak)
  • określenie tego koloru granicznego (żółty w tym przypadku)
  • skanowanie wszystkich pixeli
  • odjęcie pixeli granicznych
  • dla zadanej wartości DPI określenie efektywnego (po odjęciu powyższego) pola powierzchni

w trybie ręcznym:

  • możliwość zaznaczenia obszaru który chcemy zliczać (z uwagi na możliwość wystąpienia innych zaburzeń w tle)

PS. w załączniku przykłady takich grafik, ale konwertowane do png, ponieważ bmp nie mogę tu wstawić.

dzięki i pozdrawiam
Mariusz

0

Ja bym to zrobił tak, że lecisz w podwójnej pętli po wysokości i szerokości (z pominięciem tych narożników niebieskich) po czerwonym tle i sprawdzasz pixel po pixlu czy jego wartość RGB różni się od tych twoich "założonych" wartości. Jeśli tak to zapisujesz sobie w jakiejś tabeli dwuwymiarowej i używając odpowiednich przekształceń matematycznych wyliczasz pole powierzchni danej figury. Możesz też spróbować mając współrzędne tych punktów polecieć bezierem lub jakimś rozszerzonym narzędziem np GDI + i tam może od razu zwróci Ci wartość powierzchni beziera ;) trzebaby poszukać.

0

Histogram i procentowe określenie pola powierzchni z całości.
Albo OpenCv filtr wykrywający krawędzie i całka podwójna pole obszaru ograniczonego krzywymi.

0

Na początku mogę zdefiniować cztery znaczniki, jak na grafice poniżej. Jeżeli ich kolor jest taki sam, zmieniam narożniki na kolor tła.
title narozniki

Pytanie: Jak je zaznaczyć nie ruszając obszaru aktywnego? Można by przybliżyć zaznaczając np. dwa obszary trójkątne na każdy narożnik i zmieniając tam kolor tła. Jak zaznaczyć taki obszar trójkątny na bitmapie bazując na np pre-definiowanych (jakieś stałe np. zdefiniowane w programie) punktach tej mapy?
title

Efektem tego byłby jednolity obszar tła jak poniżej
title narozniki_tlo

Teraz czas na skanowanie w pętli każdego pixela po X i Y.
Pytanie: Jaką funkcją to zrobić?

Dalej definicja tego co jest obszarem "pomiędzy" czyli żółtym w tym przypadku. Tutaj myślałem o tym, żeby kolor zdefiniować przedziałem
Pytanie: Jak pokombinować w RGB żeby zdefiniować taki przedział koloru? Myślę, że po prostu mógłbym testować różne odcienie, zobaczyć w kodach koloru jak się to zmienia i wtedy zapodać taki "przedział" do porównania.

1
rosae:

jest bitmapa, zawsze rozmiaru 228x114, 32-bitowa z zawartością tła (czerwone), narożniki (jakiś fiolet) i częścią która mnie interesuje czyli takim wąskim paskiem (gama kolorów)

Dlaczego 32-bitowa? Nie widzę abyś używał kanału alfa, więc w zupełności wystarczy 24-bit.

chciałbym dowiedzieć się, jak mógłbym zliczać pixele z tego właśnie obszaru w kolorze różnym od tła minus wartości dla żółtego (byłaby to powiedzmy wartość graniczna)

Znasz wymiary bitmapy, znasz rozmiar pikseli (bitowość), więc spokojnie możesz do tego celu użyć ScanLine i ręcznie przeiterować po jej pikselach. Tym bardziej, że bitmapa jest malutka, więc nie potrzebujesz super optymalnego rozwiązania (choć to co oferuje ScanLine pozwoli na napisanie efektywnego kodu).

domyślam się, że znając DPI dla danego systemu, jestem w stanie (przynajmniej z grubsza) określić wartość pola powierzchni tego interesującego mnie obszaru.

W jakiej jednostce chcesz mieć określone pole powierzchni? Wystarczy obliczyć pole powierzchni całego obrazu, następnie policzyć procentowy udział danego koloru i obliczyć ten procent z całości - nic trudnego.

Co do podpunktów i trybów - funkcja licząca pole powierzchni powinna działać niezależnie od trybu i dać się wykorzystać w obu trybach. Te dwie rzeczy musisz rozdzielić, bo nie mają ze sobą wiele wspólnego.


Kod określający procentowy udziału danego koloru w bitmapie nie jest trudny do napisania. Najpierw trochę typów danych ułatwiających iterowanie po pikselach:

type
  TRGBTriple = packed record
    B, G, R: UInt8;
  end;

type
  PRGBTripleArr = ^TRGBTripleArr;
  TRGBTripleArr = packed array [0 .. MaxInt div SizeOf(TRGBTriple) - 1] of TRGBTriple;

Takie typy danych powinny znajdować się w bibliotece standardowej, ale na wszelki wypadek podaję. Jeśli koniecznie potrzebujesz używać 32-bitowych grafik to powyższe wystarczy rozbudować o dodakowy bajt dla kanału alfa (w kolejności ABGR).

No i właściwa funkcja:

function PercentageOfColor(ABitmap: TBitmap; AR, AG, AB: UInt8): Double;
var
  LLine: PRGBTripleArr;
  LLineIdx: Integer;
  LPixelIdx: Integer;
var
  LColCount: Integer = 0;
begin
  for LLineIdx := 0 to ABitmap.Height - 1 do
  begin
    LLine := ABitmap.ScanLine[LLineIdx];

    for LPixelIdx := 0 to ABitmap.Width - 1 do
      with LLine^[LPixelIdx] do
        if (R = AR) and (G = AG) and (B = AB) then
          LColCount += 1;
  end;

  Result := (LColCount * 100) / (ABitmap.Width * ABitmap.Height);
end;

Wynik zwraca jako Double, więc nie zaokrągla go, co pozwoli na dokładniejsze obliczenia w kolejnym etapie. A w dalszym etapie (gdzieś w funkcji nadrzędnej) policz pole powierzchni całości względem zadanego DPI i na podstawie uzyskanego wyniku z powyższej funkcji, oblicz powierzchnię zajmowaną przez dany kolor.

To co podałem to tylko przykład, szukający koloru o dokładnie takich samych składowych jak podane w parametrach. Jeśli funkcja ta ma sprawdzać cały zakres odcieni zadanego koloru to wystarczy zmienić warunek i ew. dostosować parametry formalne.

Kod testowałem w Lazarusie, ale powinien również bez przeszkód działać w dowolnych Delphi.

0

Dzięki FURIOUS za tą funkcję, postaram się dzisiaj pod wieczór do tego siąść i zrobić z tego użytek. Dam znać jak poszło. Pozdrawiam ;)

EDIT:
W końcu siadłem i coś tam zrobiłem. Przerzuciłem wszystko do Lazarusa na początek, prosta aplikacja otwierająca bitmapę i wrzucająca obliczenia w Memo

  1. kompilator daje warna "ScanLine is not portable", czy to w wersji oryginalnej RGB czy później zmienionej na RGBA
  2. bitmapy mam zawsze 32-bit, pewnie dlatego że urządzenie z którego to sczytuję tak je zapisuje.
  3. nie mam pojęcia jak znaleźć wartości kanału ALFA, w Paincie przy powiększeniu sczytuję wartości kolorów RGB pixeli:
    title

Zmieniłem funkcję dostosowując ją do tego co pisałeś wyżej, robiąc to dla 32-bitów RGBA

function PercentageOfColor(ABitmap: TBitmap; AA, AR, AG, AB: UInt8): Longint;

wcześniej deklaracje itp.

dalej mam określenie tych narożników i tła:

  Area_total:= BMP_file.Width*BMP_file.Height;
  Area_corners:= PercentageOfColor(BMP_file,0,0,0,160);
  Area_background:= PercentageOfColor(BMP_file,0,255,3,0);
  Area_scan:= Area_total - (Area_background + Area_corners);

wsadziłem tutaj wartość kanału alfa na początku i jako zero, co się nie sprawdza niestety..
tutaj przykład działania tego programiku:
title

  1. Określiłem metodą próbkowania (paint, haha) jaki zakres powinienem przyjąć dla tego "borderline" koloru żółtego (grafika pixelowa powyżej) i wychodzi mi: R=255, G=(252..105), B=0. Czy jest jakaś możliwość użycia tej funkcji z zakresem?

  2. Jak zaznaczyć kursorem pole aktywne? W ten sposób mógłbym określić pole "Scan_IN" oraz "Scan_OUT" i uprościć programik dla części zastosowań. W innych przypadkach i tak będę musiał analizować całość, bo te obszary aktywne będą w kilku miejscach. Mam taki pomysł, żeby sprawdzać narożniki (ale te dla tła "czerwonego") i jeśli już będę miał Scan_OUT to zmieniać wszystkie pixele w tym obszarze na kolor tła -pozbędę się "fioletowych" narożników.

pozdrawiam serdecznie

0

Cześć Panowie!
Napisze ktoś jak złapać obszar myszką, żeby określić obszar aktywny do dalszych obliczeń?
I co z tym kanałem alfa, próbowałem (powyżej) i nie zadziałało
pozdr -m

EDIT:
@Furious, próbowałem oryginalny kod tej funkcji i próbowałem kod zmieniony na RGBA, nie poszło. Może doczytam trochę i zrobię pętle z "getpixel" i jakimś porównaniem z kolorami pre-definiowanymi.

1
  1. kompilator daje warna "ScanLine is not portable", czy to w wersji oryginalnej RGB czy później zmienionej na RGBA

Tym się nie przejmuj.

  1. bitmapy mam zawsze 32-bit, pewnie dlatego że urządzenie z którego to sczytuję tak je zapisuje.

Trzeba było tak od razu... Musisz dorobić kanał alfa do podanych przeze mnie wcześniej typów. Chyba że korzystasz z jakichś wbudowanych to użyj wersji dla 32-bitowych pikseli.

  1. nie mam pojęcia jak znaleźć wartości kanału ALFA [...]

Normalnie, tak samo jak sprawdza się bajtowe wartości dla pozostałych składowych (przykład podałem wcześniej).

[...] w Paincie przy powiększeniu sczytuję wartości kolorów RGB pixeli:

Systemowy Paint nigdy nie obsługiwał przezroczystości, więc użyj innego programu, np. Gimp.

wsadziłem tutaj wartość kanału alfa na początku i jako zero, co się nie sprawdza niestety..

Sprawdziłem ten obraz i wszystkie piksele są nieprzezroczyste, więc wygląda na to, że kanał alfa w ogóle nie jest używany. Pod lupę wziąłem obraz PNG, który dodałeś do załączników w pierwszym poście.

  1. Określiłem metodą próbkowania (paint, haha) jaki zakres powinienem przyjąć dla tego "borderline" koloru żółtego (grafika pixelowa powyżej) i wychodzi mi: R=255, G=(252..105), B=0. Czy jest jakaś możliwość użycia tej funkcji z zakresem?

Zależy w jaki sposób chciałbyś przekazać taki zakres. Zasadniczo dla każdej składowej można przydzielić dwa parametry typu UInt8 - jeden dla minimum a drugi dla maksimum. Możesz doprowadzić kod do takiej postaci:

if (A in [AMin .. AMax]) and (B in [BMin .. BMax]) and 
   (G in [GMin .. GMax]) and (R in [RMin .. RMax]) then

Wszystkie *Min i *Max (jest ich sześć) podać w parametrach. Albo zakres dać tylko dla składowej G:

if (A = AA) and (B = AB) and (G in [AGMin .. AGMax]) and (R = AR) then

Wszystko zależy od tego jak chcesz aby wyglądały parametry.

  1. Jak zaznaczyć kursorem pole aktywne? [...]

A jakiego komponentu używasz?

0

dzięki za info ;)
kilka bitmap oryginanych wsadziłem w ZIPa
używałem w Lazarusie zwykłego TImage + OpenPictureDialog

to pole aktywne przyda sie, jak zobaczysz jedna z bitmap - tam są dwie ścieżki i możnaby wybrać jedną do oceny

1

A jednak nie - istnieją przezroczyste piksele :]

Poniższa funkcja zlicza piksele:

uses
  Graphics;

type
  TRGBAPixel = record
    Alpha, B, G, R: UInt8;
  end;

type
  PRGBAPixelsRow = ^TRGBAPixelsRow;
  TRGBAPixelsRow = array [0 .. MaxInt div SizeOf(TRGBAPixel) - 1] of TRGBAPixel;

  function TransparentPixelsCount(APicture: TPortableNetworkGraphic): Integer;
  var
    LRow: PRGBAPixelsRow;
    LRowIdx, LPixelIdx: Integer;
  begin
    Result := 0;

    for LRowIdx := 0 to APicture.Height - 1 do
    begin
      LRow := APicture.ScanLine[LRowIdx];

      for LPixelIdx := 0 to APicture.Width -  1 do
        if LRow^[LPixelIdx].Alpha < 255 then
          Result += 1;
    end;
  end;

Jej wynik dla obrazu z pierwszego posta to 25555 - tyle pikseli w obrazie posiada obniżoną wartość kanału alfa, czyli tyle pikseli jest lekko przezroczystych.

Edit: Chyba ten kod źle liczy to co ma liczyć - sprawdziłem w programie graficznym i nie wydaje mi się, aby ten plik posiadał aż tak dużo półprzezroczystych pikseli. Dziś już nie mam głowy do tego - jutro coś podłubię.

0

Próbowałem określić tą przeźroczystość w GIMP-ie i tutaj jest wynik:
title
Nie umiem znaleźć wartości kanału Alpha w tych bitmapach.. gdzie tego szukać? Postaram się ten bajt zapchać "0" albo "255" na razie

Tutaj dla przykładu określony kolor narożnika; jak pisałem wcześniej będę chciał najpierw "zamalować" narożniki w kolorze tła a później dopiero zliczać pixele, bo (jak widać powyżej) zdarza się, że kolor narożników występuje także w tym obszarze który chcę przeanalizować.

EDIT:
Poniżej PNG zamiast BMP i tutaj faktycznie jest kanał Alpha
title
Co ciekawe, jak skanowałem cały obrazek, to wygląda na to że Alpha zawsze jest pełne, czyli 255

EDIT2:
Padłem jeszcze w innym miejscu: Mam określone dwa pixele, powiedzmy narożnik (poniżej) i fragment tła

function CornerColor(ABitmap: TBitmap): TColor;
var
     CornerPosX, CornerPosY: integer;
begin
     CornerPosX:= ABitmap.Width-1;
     CornerPosY:= ABitmap.Height-1;
     Result:= ABitmap.Canvas.Pixels[CornerPosX, CornerPosY];
end; 

jak teraz z TColor przejść do RGBTriple?

próbuję jako składową wyjąć np Czerwony:

zmienne takie:

CornerR, CornerG, CornerB, BckgndR, BckgndG, BckgndB: UInt8;

dalej próbuję coś takiego wywołać:

  CornerR:= CornerColor(BMP_file).rgbtRed; 

i dostaję "Illegal qualifier". Gdzie się walnąłem?

EDIT3:
spróbowałem jeszcze raz ogarnąć tą bitmapę, jako zwykłą 24-bit (tak jak na początku podałeś funkcję) i poszło ;)
do tego zrobiłem zliczanie pixeli, określanie powierzchni w procentach (chyba zostanę przy ilości pixeli -sam zobacz, jak małe to ilości) oraz zakres do określania żółtych odcieni i dla sprawdzenia odczyt wartości "TColor" w jednym punkcje narożnika oraz jednym punkcie tła.
efekt taki:
title

kilka innych bitmap ma lekko różniące się kolory w narożnikach i ten czerwony tła -może to jest przyczyną zerowych odczytów?

1

Ale które obrazy są tymi oryginalnymi? .bmp czy .png? Ja dla testu pobrałem .png z pierwszego posta i na nim coś dłubałem. We wszystkich kolejnych postach podawałeś .bmp, więc nie wiem czy te pliki są surowe czy już po jakiejś obróbce.

Nie umiem znaleźć wartości kanału Alpha w tych bitmapach.. gdzie tego szukać? Postaram się ten bajt zapchać "0" albo "255" na razie

Jak chcesz usunąć kanał alfa to możesz to zrobić w dowolnym programie graficznym ten kanał obsługującym. Pozbywając się przezroczystości, będziesz mógł już bez problemu operować na obrazie 24-bitowym, obojętne w jakim formacie.

Tutaj dla przykładu określony kolor narożnika; jak pisałem wcześniej będę chciał najpierw "zamalować" narożniki w kolorze tła a później dopiero zliczać pixele, bo (jak widać powyżej) zdarza się, że kolor narożników występuje także w tym obszarze który chcę przeanalizować.

Kolor każdego narożnika możesz określić na dwa sposoby - albo za pomocą właściwości Pixels, albo już po pobraniu wskaźnika na wiersz za pomocą ScanLine. A jeśli wszystkie rogi obrazu zawsze są tego samego koloru (czyli nie jest tak, że jeden róg ma kolor niebieski, a inny fioletowy) to wystarczy pobrać ten kolor raz, z dowolnego rogu.

Aby zamalować rogi wystarczy cztery razy użyć metody Canvas.FloodFill - metoda ta w parametrze typu TColor pobiera kolor do zamalowania, więc do niej będzie pasować właściwość Pixels.

jak teraz z TColor przejść do RGBTriple?

Co to znaczy "przejść"? Rzutowaniem raczej się tego nie załatwi, więc potrzebna będzie konwersja.

kilka innych bitmap ma lekko różniące się kolory w narożnikach i ten czerwony tła -może to jest przyczyną zerowych odczytów?

W takim razie ten czerwony kolor też musisz pobierać, zamiast hardkodować jego wartość.

0

Witam ponownie,

trochę czasu minęło, miałem w pracy sporo do zrobienia i teraz dopiero wracam do tego programiku.
oryginalne są pliki BMP które załączyłem w zip-ie, wszystkie PNG to już konwersja, bo nie mogłem tu wsadzić gołego BMP w tekście forum.
tak, rogi są zawsze takiego samego koloru -ale nie mogę chyba zrobić zamiany tego koloru, bo na części roboczej wykresu też czasem taki kolor się trafia..

Na teraz mam dwa problemy jeszcze do przeskoczenia:

  • ta zamiana, czy konwersja typu TColor do TRGBTriple, żebym mógł dla wybranych pixeli określić ich składowe RGB (jeden narożnik i jakiś pixel z brzegu tła)
  • w jaki sposób użyć zaznaczenia myszą, żeby określić obszar aktywny, taki Area_IN i Area_OUT

pozdrawiam
Mariusz

1

oryginalne są pliki BMP które załączyłem w zip-ie, wszystkie PNG to już konwersja, bo nie mogłem tu wsadzić gołego BMP w tekście forum.

Zawsze możesz je spakować do .zip i dorzucić do załączników.

tak, rogi są zawsze takiego samego koloru -ale nie mogę chyba zrobić zamiany tego koloru, bo na części roboczej wykresu też czasem taki kolor się trafia..

Możesz i napisałem Ci w poprzedniej wiadomości jak to zrobić. Nie chodzi o to, aby pobrać kolor rogów i w pętli pozamieniać wszystkie piksele w grafice o takim kolorze na inny - w ten sposób wykasujesz również kolor z wnętrza mapy. Trzeba to zrobić w taki sam sposób, w jakim wykonywane jest wypełnianie danym kolorem w programach graficznych (np. w systemowym Paint), czyli skorzystać z algorytmu rozrostu ziarna. Na szczęście nie trzeba go implementować samemu, bo w klasie TCanvas już taki istnieje - to właśnie zadanie metody FloodFill.

  • ta zamiana, czy konwersja typu TColor do TRGBTriple, żebym mógł dla wybranych pixeli określić ich składowe RGB (jeden narożnik i jakiś pixel z brzegu tła)

Możesz to zrobić ręcznie za pomocą operatorów koniunkcji (and) i przesunięcia bitowego (shr):

R := AColor and $0000FF;
G := AColor and $00FF00 shr 8;
B := AColor and $FF0000 shr 16;

Gdzie AColor to zmienna typu TColor. Można też skorzystać z gotowej procedury do wyodrębniania tych składowych. Nie wiem jak to wygląda w Delphi, ale w Lazarusie jest do tego procedura Graphics.RedGreenBlue i robi dokładnie to samo. A w drugą stronę, czyli zbudowanie wartości typu TColor z trzech składowych, można zrealizować znów za pomocą operatorów:

AColor := (B shl 16) or (G shl 8) or R;

lub użyć do tego funkcji Graphics.RGBToColor, która robi to samo.

  • w jaki sposób użyć zaznaczenia myszą, żeby określić obszar aktywny, taki Area_IN i Area_OUT

Zależy w czym sobie wyświetlasz tę bitmapkę i również zależy od tego, jaki efekt Cię interesuje. Czy ramka zaznaczania ma być widoczna podczas samego zaznaczania, jak i po jego zakończeniu (jak np. w Paint), czy ma być niewidoczna. Zasadniczo sprowadza się to do obsługi kilku zdarzeń:

  • OnMouseDown - wciśnięcie przycisku myszy, oprogramowanie rozpoczęcia zaznaczania,
  • OnMouseMove - przesuwanie kursora, oprogramowanie dynamicznego rysowania ramki zaznaczenia,
  • OnMouseUp - zwolnienie przycisku myszy, oprogramowanie zakończenia zaznaczania.

Pamiętaj, że jeśli chcesz dynamicznie malować taką ramkę to obraz bitmapy musi sobie siedzieć w jakiejś zmiennej, a w komponencie trzeba wyświetlić najpierw bitmapkę ze zmiennej, a następnie domalować na niej ramkę. Ramkę należy malować wyłącznie na płótnie komponentu, tak aby nie zamazać właściwego obrazu.


Napisałem taką prostą aplikację do wizualnego zaznaczania obszaru na komponencie typu TPaintBox. Bitmapa z pliku siedzi w oryginale w polu klasy, wyświetlana jest w komponencie, a dodatkowo, podczas zaznaczania i po zaznaczeniu widoczna jest ramka zaznaczenia (tylko w komponencie, na bitmapie jej nie ma, bo być jej nie może).

Działa to w ten sposób, że zaznaczanie realizowane jest przez obsługę lewego przycisku myszy (prawy nie jest obsługiwany - dla niego można dorobić inną opcję, np. wyświetlenie menu kontekstowego). Podczas zaznaczania rysowana jest kropkowana ramka w kolorze jasnoszarym, a po zwolnieniu przycisku i tym samym zakończeniu zaznaczania, rysowana jest ramka w kolorze białym. Kliknięcie na komponencie to usunięcie ramki.

Po kolei:

type
  TSelectingMode = (smNone, smSelecting, smSelected);

Typ określający tryb zaznaczania - smNone to brak zaznaczania (nic się nie dzieje, ramka nie jest zaznaczona), smSelecting oznacza trwanie zaznaczania, smSelected oznacza stan po zakończeniu zaznaczania.

private
  FHeatMap: TBitmap;
  FSelectingMode: TSelectingMode;
  FSelectedRect: TRect;

FHeatMap to bitmapa z oryginalnym obrazem załadowanym z pliku, FSelectingMode przechowuje bieżący tryb zaznaczania, a FSelectedRect przechowuje obszar zaznaczenia - z niego będziesz mógł skorzystać po zaznaczeniu, np. w celu skopiowania fragmentu obrazu do osobnej bitmapy.

operator = (const ALeft, ARight: TPoint) Return: Boolean;
begin
  Result := (ALeft.X = ARight.X) and (ALeft.Y = ARight.Y);
end;

W Lazarusie 1.6.2 typ TPoint nie ma zdefiniowanego zachowania dla tego operatora, więc przeciążyłem go sobie. Chyba że funkcja ta zdefiniowana jest w module Types, ale tego nie dołączałem, bo nie było takiej potrzeby. Nie pamiętam czego używasz, ale być może nie trzeba tego robić.

I kod klasy formularza:

procedure TMainForm.FormCreate(ASender: TObject);
begin
  FHeatMap := TBitmap.Create();
  FHeatMap.LoadFromFile('map.bmp');

  FSelectingMode := smNone;
end;

Utworzenie bitmapy w pamięci, załadowanie jej z pliku oraz ustawienie trybu zaznaczenia na... brak zaznaczenia.

procedure TMainForm.FormDestroy(ASender: TObject);
begin
  FHeatMap.Free();
end;

Zwolnienie obiektu bitmapy z pamięci podczas usuwania z pamięci obiektu formularza.

procedure TMainForm.CPaintBoxPaint(ASender: TObject);
var
  LBox: TPaintBox absolute ASender;
begin
  LBox.Canvas.Draw(0, 0, FHeatMap);

  if FSelectingMode <> smNone then
    with LBox.Canvas do
    begin
      case FSelectingMode of
        smSelecting: Pen.Color := clLtGray;
        smSelected:  Pen.Color := clWhite;
      end;

      Pen.Style := psDot;
      Brush.Style := bsClear;

      Rectangle(FSelectedRect);
    end;
end;

Zdarzenie malowania komponentu. Najpierw malowana jest bitmapa na całej powierzchni kontrolki. Następnie sprawdzane jest, czy użytkownik zaznacza lub zaznaczył obszar i jeśli tak, to:

  • ustawiany jest kolor "ołówka" - jeśli użytkownik zaznacza obszar to ustawiany jest kolor jasnoszary, a jeśli już zaznaczył to kolor biały,
  • ustawienie stylu ołówka na punktowy (ramka będzie kropkowana) oraz stylu wypełnienia na pusty (wnętrze obszaru nie zostanie zamalowane),
  • wypełnienie obszaru na podstawie ustawień płótna (ołówka i wypełnienia).
procedure TMainForm.CPaintBoxMouseDown(ASender: TObject; AButton: TMouseButton; AShift: TShiftState; AX, AY: Integer);
var
  LBox: TPaintBox absolute ASender;
begin
  if AButton = mbLeft then
  begin
    FSelectingMode := smSelecting;
    FSelectedRect := Rect(AX, AY, AX, AY);

    LBox.Invalidate();
  end;
end;

Obsługa wciśnięcia przycisku myszy. Najpierw sprawdzane jest czy użytkownik wcisnął LPM i jeśli tak, następuje:

  • ustawienie trybu na smSelecting, czyli trwanie zaznaczania,
  • ustawienie danych obszaru zaznaczenia - w tym miejscu lewy górny róg obszaru jest taki sam jak prawy dolny róg tego obszaru,
  • przemalowanie komponentu.
procedure TMainForm.CPaintBoxMouseMove(ASender: TObject; AShift: TShiftState; AX, AY: Integer);
var
  LBox: TPaintBox absolute ASender;
begin
  if FSelectingMode = smSelecting then
  begin
    FSelectedRect.BottomRight := Point(AX, AY);
    LBox.Invalidate();
  end;
end;

Obsługa ruchu kursora nad komponentem. Obsługa aktywna jest tylko i wyłącznie w przypadku, gdy właśnie trwa zaznaczanie, więc musi być ustawiony tryb smSelecting - w innym przypadku nic nie powinno być wykonywane. Jeśli trwa zaznaczanie to aktualizujemy prawy dolny róg obszaru na podstawie parametrów AX i AY, a także przemalowujemy komponent, aby ramka zaznaczenia była na bieżąco aktualizowana na ekranie.

procedure TMainForm.CPaintBoxMouseUp(ASender: TObject; AButton: TMouseButton; AShift: TShiftState; AX, AY: Integer);
var
  LBox: TPaintBox absolute ASender;
begin
  if AButton = mbLeft then
  begin
    if FSelectedRect.TopLeft = FSelectedRect.BottomRight then
      FSelectingMode := smNone
    else
      FSelectingMode := smSelected;

    LBox.Invalidate();
  end;
end;

Obsługa zwolnienia przycisku myszy. Jeśli użytkownik zwolnił lewy przycisk myszy to sprawdzamy, czy cokolwiek zaznaczył. Jeśli tylko kliknął (czyli nic nie zaznaczył) to ustawiamy tryb na smNone (brak zaznaczenia), a jeśli coś zaznaczył to ustawiamy tryb na smSelected. Na koniec odmalowujemy komponent, aby ramka została namalowana w białym kolorze.

Podczas zaznaczania będziesz widział coś takiego (kursora nie widać na zrzutach - jest obok prawego dolnego rogu ramki):

selecting.png

A po zaznaczeniu coś takiego:

selected.png

Do malowania zaznaczenia można też użyć metody TCanvas.DrawFocusRect - wtedy ramka będzie malowana z kropek, a kolor każdego piksela będzie odwrotny do koloru tła. Czyli zwykły xor, otrzymany w gratisie.

To tyle - jak widać obsługa zaznaczania obszaru nie jest wcale taka trudna. To co podałem wyżej to tylko podstawy zaznaczania - ten mechanizm można wzbogacić o inne opcje oraz jakieś fajne bajery. W każdym razie, w załączniku podaję pełne źródła napisane w Lazarusie (Delphi nie mam, nie używam), więc możesz sobie zobaczyć jak to wygląda w całości. Dodaję też plik wykonywalny, abyś mógł sprawdzić działanie bez kompilacji projektu.

Have fun :]

1 użytkowników online, w tym zalogowanych: 0, gości: 1